From 6d72014442bba202b3c31ed995691ef1fa367458 Mon Sep 17 00:00:00 2001 From: Ares <75481906+ice-ares@users.noreply.github.com> Date: Thu, 23 Mar 2023 07:17:54 +0200 Subject: [PATCH] repo made publicly available --- .github/CODEOWNERS | 1 + .github/workflows/CICD.yaml | 573 ++++ .gitignore | 27 + .golangci.yml | 546 +++ LICENSE.header | 1 + LICENSE.txt | 50 + Makefile | 256 ++ README.md | 43 + application.yaml | 189 ++ .../.testdata/application.yaml | 18 + .../.testdata/expected_swagger.json | 0 .../.testdata/localhost.crt | 21 + .../.testdata/localhost.key | 28 + cmd/freezer-refrigerant/Dockerfile | 28 + cmd/freezer-refrigerant/api/docs.go | 506 +++ cmd/freezer-refrigerant/api/swagger.json | 485 +++ cmd/freezer-refrigerant/api/swagger.yaml | 337 ++ cmd/freezer-refrigerant/contract.go | 55 + .../freezer_refrigerant.go | 77 + cmd/freezer-refrigerant/tokenomics.go | 160 + cmd/freezer/.testdata/application.yaml | 18 + cmd/freezer/.testdata/expected_swagger.json | 0 cmd/freezer/.testdata/localhost.crt | 21 + cmd/freezer/.testdata/localhost.key | 28 + cmd/freezer/Dockerfile | 28 + cmd/freezer/api/docs.go | 895 +++++ cmd/freezer/api/swagger.json | 874 +++++ cmd/freezer/api/swagger.yaml | 600 ++++ cmd/freezer/contract.go | 70 + cmd/freezer/freezer.go | 79 + cmd/freezer/statistics.go | 82 + cmd/freezer/tokenomics.go | 228 ++ go.mod | 145 + go.sum | 863 +++++ local.go | 45 + tokenomics/.testdata/application.yaml | 166 + tokenomics/DDL.lua | 209 ++ tokenomics/adoption.go | 247 ++ tokenomics/balance.go | 571 ++++ tokenomics/balance_recalculation.go | 585 ++++ ...verse_total_no_pre_staking_bonus_amount.go | 192 ++ ...verse_total_no_pre_staking_bonus_amount.go | 187 ++ ...on_t0_total_no_pre_staking_bonus_amount.go | 163 + ...on_t1_total_no_pre_staking_bonus_amount.go | 141 + ...on_t2_total_no_pre_staking_bonus_amount.go | 141 + tokenomics/balance_recalculation_test.go | 1118 +++++++ ...ation_total_no_pre_staking_bonus_amount.go | 80 + ...ation_total_no_pre_staking_bonus_amount.go | 151 + ...ation_total_no_pre_staking_bonus_amount.go | 61 + tokenomics/balance_test.go | 2918 +++++++++++++++++ .../blockchain_balance_synchronization.go | 307 ++ tokenomics/contract.go | 406 +++ tokenomics/extra_bonus.go | 245 ++ tokenomics/extra_bonus_processing.go | 182 + tokenomics/fixture/contract.go | 25 + tokenomics/fixture/fixture.go | 74 + tokenomics/global.go | 51 + tokenomics/mining.go | 598 ++++ tokenomics/mining_rates_recalculation.go | 329 ++ tokenomics/mining_sessions.go | 368 +++ tokenomics/mining_sessions_test.go | 480 +++ tokenomics/mining_test.go | 39 + tokenomics/pre_staking.go | 137 + tokenomics/seeding/seeding.go | 60 + tokenomics/tokenomics.go | 258 ++ tokenomics/users.go | 317 ++ tokenomics/worker.go | 80 + 67 files changed, 18263 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/CICD.yaml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 LICENSE.header create mode 100644 LICENSE.txt create mode 100644 Makefile create mode 100644 README.md create mode 100644 application.yaml create mode 100644 cmd/freezer-refrigerant/.testdata/application.yaml create mode 100644 cmd/freezer-refrigerant/.testdata/expected_swagger.json create mode 100644 cmd/freezer-refrigerant/.testdata/localhost.crt create mode 100644 cmd/freezer-refrigerant/.testdata/localhost.key create mode 100644 cmd/freezer-refrigerant/Dockerfile create mode 100644 cmd/freezer-refrigerant/api/docs.go create mode 100644 cmd/freezer-refrigerant/api/swagger.json create mode 100644 cmd/freezer-refrigerant/api/swagger.yaml create mode 100644 cmd/freezer-refrigerant/contract.go create mode 100644 cmd/freezer-refrigerant/freezer_refrigerant.go create mode 100644 cmd/freezer-refrigerant/tokenomics.go create mode 100644 cmd/freezer/.testdata/application.yaml create mode 100644 cmd/freezer/.testdata/expected_swagger.json create mode 100644 cmd/freezer/.testdata/localhost.crt create mode 100644 cmd/freezer/.testdata/localhost.key create mode 100644 cmd/freezer/Dockerfile create mode 100644 cmd/freezer/api/docs.go create mode 100644 cmd/freezer/api/swagger.json create mode 100644 cmd/freezer/api/swagger.yaml create mode 100644 cmd/freezer/contract.go create mode 100644 cmd/freezer/freezer.go create mode 100644 cmd/freezer/statistics.go create mode 100644 cmd/freezer/tokenomics.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 local.go create mode 100644 tokenomics/.testdata/application.yaml create mode 100644 tokenomics/DDL.lua create mode 100644 tokenomics/adoption.go create mode 100644 tokenomics/balance.go create mode 100644 tokenomics/balance_recalculation.go create mode 100644 tokenomics/balance_recalculation_t-1_reverse_total_no_pre_staking_bonus_amount.go create mode 100644 tokenomics/balance_recalculation_t0_reverse_total_no_pre_staking_bonus_amount.go create mode 100644 tokenomics/balance_recalculation_t0_total_no_pre_staking_bonus_amount.go create mode 100644 tokenomics/balance_recalculation_t1_total_no_pre_staking_bonus_amount.go create mode 100644 tokenomics/balance_recalculation_t2_total_no_pre_staking_bonus_amount.go create mode 100644 tokenomics/balance_recalculation_test.go create mode 100644 tokenomics/balance_recalculation_this_duration_total_no_pre_staking_bonus_amount.go create mode 100644 tokenomics/balance_recalculation_total_no_pre_staking_bonus_amount.go create mode 100644 tokenomics/balance_recalculation_until_this_duration_total_no_pre_staking_bonus_amount.go create mode 100644 tokenomics/balance_test.go create mode 100644 tokenomics/blockchain_balance_synchronization.go create mode 100644 tokenomics/contract.go create mode 100644 tokenomics/extra_bonus.go create mode 100644 tokenomics/extra_bonus_processing.go create mode 100644 tokenomics/fixture/contract.go create mode 100644 tokenomics/fixture/fixture.go create mode 100644 tokenomics/global.go create mode 100644 tokenomics/mining.go create mode 100644 tokenomics/mining_rates_recalculation.go create mode 100644 tokenomics/mining_sessions.go create mode 100644 tokenomics/mining_sessions_test.go create mode 100644 tokenomics/mining_test.go create mode 100644 tokenomics/pre_staking.go create mode 100644 tokenomics/seeding/seeding.go create mode 100644 tokenomics/tokenomics.go create mode 100644 tokenomics/users.go create mode 100644 tokenomics/worker.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..95ba11d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ice-blockchain/golang diff --git a/.github/workflows/CICD.yaml b/.github/workflows/CICD.yaml new file mode 100644 index 0000000..0c938de --- /dev/null +++ b/.github/workflows/CICD.yaml @@ -0,0 +1,573 @@ +# SPDX-License-Identifier: ice License 1.0 + +name: CI/CD +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + verify-licensing: + name: Verify Licensing + if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Find latest Go Version + id: findLatestGoVersion + run: | + LATEST_GO_VERSION=$(make latestGoVersion) + echo "::set-output name=latestGoVersion::$LATEST_GO_VERSION" + - name: Setup GO + uses: actions/setup-go@v3 + with: + go-version: ${{ steps.findLatestGoVersion.outputs.latestGoVersion }} + - name: Check License + run: make checkLicense + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + https://github.com/ice-blockchain/${{ github.event.repository.name }} + verify-latest-versions-used: + name: Verify Latest Versions + if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Find latest Go Version + id: findLatestGoVersion + run: | + LATEST_GO_VERSION=$(make latestGoVersion) + echo "::set-output name=latestGoVersion::$LATEST_GO_VERSION" + - name: Setup GO + uses: actions/setup-go@v3 + with: + go-version: ${{ steps.findLatestGoVersion.outputs.latestGoVersion }} + - name: Check If We're Up to Date with Everything + run: make checkModVersion checkIfAllDependenciesAreUpToDate + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + https://github.com/ice-blockchain/${{ github.event.repository.name }} + verify-auto-generated: + name: Verify Auto Generated + if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Find latest Go Version + id: findLatestGoVersion + run: | + LATEST_GO_VERSION=$(make latestGoVersion) + echo "::set-output name=latestGoVersion::$LATEST_GO_VERSION" + - name: Setup GO + uses: actions/setup-go@v3 + with: + go-version: ${{ steps.findLatestGoVersion.outputs.latestGoVersion }} + - name: Check Auto Generated Files + run: make checkGenerated + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + https://github.com/ice-blockchain/${{ github.event.repository.name }} + lint: + name: Lint + if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Find latest Go Version + id: findLatestGoVersion + run: | + LATEST_GO_VERSION=$(make latestGoVersion) + echo "::set-output name=latestGoVersion::$LATEST_GO_VERSION" + - name: Setup GO + uses: actions/setup-go@v3 + with: + go-version: ${{ steps.findLatestGoVersion.outputs.latestGoVersion }} + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + https://github.com/ice-blockchain/${{ github.event.repository.name }} + build: + name: Build + if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Find latest Go Version + id: findLatestGoVersion + run: | + LATEST_GO_VERSION=$(make latestGoVersion) + echo "::set-output name=latestGoVersion::$LATEST_GO_VERSION" + - name: Setup GO + uses: actions/setup-go@v3 + with: + go-version: ${{ steps.findLatestGoVersion.outputs.latestGoVersion }} + - name: Build all + run: make build-all@ci/cd + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + https://github.com/ice-blockchain/${{ github.event.repository.name }} + test: + name: Test + strategy: + matrix: + package: [ "tokenomics", "cmd/freezer", "cmd/freezer-refrigerant"] + if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'push' }} + runs-on: ubuntu-latest + # runs-on: self-hosted-ubuntu-latest-x64 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Find latest Go Version + id: findLatestGoVersion + run: | + LATEST_GO_VERSION=$(make latestGoVersion) + echo "::set-output name=latestGoVersion::$LATEST_GO_VERSION" + - name: Setup GO + uses: actions/setup-go@v3 + with: + go-version: ${{ steps.findLatestGoVersion.outputs.latestGoVersion }} + - name: Test ${{ matrix.package }} + env: + SMS_CLIENT_USER: ${{ secrets.SMS_CLIENT_USER }} + SMS_CLIENT_PASSWORD: ${{ secrets.SMS_CLIENT_PASSWORD }} + EMAIL_CLIENT_APIKEY: ${{ secrets.EMAIL_CLIENT_APIKEY }} + PICTURE_STORAGE_ACCESS_KEY: ${{ secrets.PICTURE_STORAGE_ACCESS_KEY }} + INAPP_NOTIFICATIONS_CLIENT_KEY: ${{ secrets.INAPP_NOTIFICATIONS_CLIENT_KEY }} + INAPP_NOTIFICATIONS_CLIENT_SECRET: ${{ secrets.INAPP_NOTIFICATIONS_CLIENT_SECRET }} + INAPP_NOTIFICATIONS_CLIENT_APP_ID: ${{ secrets.INAPP_NOTIFICATIONS_CLIENT_APP_ID }} + TRANSLATIONS_CLIENT_APIKEY: ${{ secrets.TRANSLATIONS_CLIENT_APIKEY }} + GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} + GCP_FIREBASE_AUTH_API_KEY: ${{ secrets.GCP_FIREBASE_AUTH_API_KEY }} + run: | + cd ${{ matrix.package }} + make -f ${{ github.workspace }}/Makefile test@ci/cd + make -f ${{ github.workspace }}/Makefile coverage + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + freezer/${{ matrix.package }} + https://github.com/ice-blockchain/${{ github.event.repository.name }} + benchmark: + name: Benchmark + strategy: + matrix: + package: [ "tokenomics", "cmd/freezer", "cmd/freezer-refrigerant"] + if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'push' }} + runs-on: ubuntu-latest + # runs-on: self-hosted-ubuntu-latest-x64 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Find latest Go Version + id: findLatestGoVersion + run: | + LATEST_GO_VERSION=$(make latestGoVersion) + echo "::set-output name=latestGoVersion::$LATEST_GO_VERSION" + - name: Setup GO + uses: actions/setup-go@v3 + with: + go-version: ${{ steps.findLatestGoVersion.outputs.latestGoVersion }} + - name: Benchmark ${{ matrix.package }} + env: + SMS_CLIENT_USER: ${{ secrets.SMS_CLIENT_USER }} + SMS_CLIENT_PASSWORD: ${{ secrets.SMS_CLIENT_PASSWORD }} + EMAIL_CLIENT_APIKEY: ${{ secrets.EMAIL_CLIENT_APIKEY }} + PICTURE_STORAGE_ACCESS_KEY: ${{ secrets.PICTURE_STORAGE_ACCESS_KEY }} + INAPP_NOTIFICATIONS_CLIENT_KEY: ${{ secrets.INAPP_NOTIFICATIONS_CLIENT_KEY }} + INAPP_NOTIFICATIONS_CLIENT_SECRET: ${{ secrets.INAPP_NOTIFICATIONS_CLIENT_SECRET }} + INAPP_NOTIFICATIONS_CLIENT_APP_ID: ${{ secrets.INAPP_NOTIFICATIONS_CLIENT_APP_ID }} + TRANSLATIONS_CLIENT_APIKEY: ${{ secrets.TRANSLATIONS_CLIENT_APIKEY }} + GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} + GCP_FIREBASE_AUTH_API_KEY: ${{ secrets.GCP_FIREBASE_AUTH_API_KEY }} + run: | + cd ${{ matrix.package }} + make -f ${{ github.workspace }}/Makefile benchmark@ci/cd + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + freezer/${{ matrix.package }} + https://github.com/ice-blockchain/${{ github.event.repository.name }} + dockerfile: + name: Verify Dockerfile + strategy: + matrix: + service: [ "freezer", "freezer-refrigerant"] + #those are not supported by golang docker image: linux/riscv64 + #platforms: linux/s390x,linux/arm64,linux/amd64,linux/ppc64le + #commented because build takes too damn much with the other 3 platforms (~10 mins for each!!!) and we don`t need them atm + platform: ["linux/amd64"] + if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'push' }} + runs-on: ubuntu-latest + # runs-on: self-hosted-ubuntu-latest-x64 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Find latest Go Version + id: findLatestGoVersion + run: | + LATEST_GO_VERSION=$(make latestGoVersion) + echo "::set-output name=latestGoVersion::$LATEST_GO_VERSION" + - name: Setup GO + uses: actions/setup-go@v3 + with: + go-version: ${{ steps.findLatestGoVersion.outputs.latestGoVersion }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - id: buildx + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + version: latest + install: true + - name: Build Image Information + id: build-image-info + env: + PR_NUMBER: ${{ github.event.number }} + SERVICE_NAME: ${{ matrix.service }} + run: | + echo "::set-output name=tags::registry.digitalocean.com/ice-io/$SERVICE_NAME:pr$PR_NUMBER" + echo "::set-output name=dockerFileLocation::./cmd/$SERVICE_NAME/Dockerfile" + - name: Build ${{ matrix.platform }} ${{ matrix.service }} docker image + uses: docker/build-push-action@v3 + with: + context: . + file: ${{ steps.build-image-info.outputs.dockerFileLocation }} + platforms: ${{ matrix.platform }} + push: false + build-args: | + SERVICE_NAME=${{ matrix.service }} + tags: ${{ steps.build-image-info.outputs.tags }} + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + ${{ matrix.service }} + PR: + needs: [verify-licensing, verify-latest-versions-used, verify-auto-generated, lint, build, test, benchmark, dockerfile] + name: PR + if: ${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - name: Checking if all previous jobs were successful + env: + ALL_SUCCESSFUL: ${{ contains(join(needs.*.result, ','), 'failure') == false && contains(join(needs.*.result, ','), 'cancelled') == false && contains(join(needs.*.result, ','), 'skipped') == false }} + run: | + if [ $ALL_SUCCESSFUL == 'true' ] + then + exit 0 + else + exit 1 + fi + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + https://github.com/ice-blockchain/${{ github.event.repository.name }} + release: + needs: [PR] + name: Release + outputs: + nextTag: ${{ steps.check_tag.outputs.next_tag }} + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Compute new Tag version + id: compute_tag + uses: craig-day/compute-tag@v14 + with: + github_token: ${{ github.token }} + version_type: minor + - id: check_tag + name: Check new computed Tag version + run: | + NEW_TAG_VERSION=$(echo "$NEW_TAG_VERSION" | sed 's/.\{2\}$//') + if [ $NEW_TAG_VERSION == 'v1.0.0' ] + then + echo "::set-output name=next_tag::$NEW_TAG_VERSION" + else + echo "::set-output name=next_tag::${NEW_TAG_VERSION}.0" + fi + env: + NEW_TAG_VERSION: ${{ steps.compute_tag.outputs.next_tag }} + - name: Create Release + id: create_release + #TODO This needs to be replaced ASAP! https://github.com/actions/create-release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.check_tag.outputs.next_tag }} + release_name: ${{ steps.check_tag.outputs.next_tag }} + draft: false + prerelease: false + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + https://github.com/ice-blockchain/${{ github.event.repository.name }} + push_docker: + needs: [ release ] + name: Push Docker + strategy: + matrix: + service: [ "freezer", "freezer-refrigerant"] + #those are not supported by golang docker image: linux/riscv64 + #platforms: linux/s390x,linux/arm64,linux/amd64,linux/ppc64le + #commented because build takes too damn much with the other 3 platforms (~10 mins for each!!!) and we don`t need them atm + platform: ["linux/amd64"] + outputs: + nextTag: ${{ steps.build-image-info.outputs.nextTag }} + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + # runs-on: self-hosted-ubuntu-latest-x64 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Find latest Go Version + id: findLatestGoVersion + run: | + LATEST_GO_VERSION=$(make latestGoVersion) + echo "::set-output name=latestGoVersion::$LATEST_GO_VERSION" + - name: Setup GO + uses: actions/setup-go@v3 + with: + go-version: ${{ steps.findLatestGoVersion.outputs.latestGoVersion }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - id: buildx + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + version: latest + install: true + - name: Login to Digital Ocean Container Registry + uses: docker/login-action@v2 + with: + registry: registry.digitalocean.com + username: ${{ secrets.DO_CONTAINER_REGISTRY_TOKEN }} + password: ${{ secrets.DO_CONTAINER_REGISTRY_TOKEN }} + - name: Build Image Information + id: build-image-info + env: + VERSION: ${{needs.release.outputs.nextTag}} + SERVICE_NAME: ${{ matrix.service }} + run: | + LATEST_TAG=registry.digitalocean.com/ice-io/$SERVICE_NAME:latest + NEXT_VERSION_TAG=registry.digitalocean.com/ice-io/$SERVICE_NAME:$VERSION + echo "::set-output name=nextTag::$VERSION" + echo "::set-output name=tags::${LATEST_TAG},${NEXT_VERSION_TAG}" + echo "::set-output name=dockerFileLocation::./cmd/$SERVICE_NAME/Dockerfile" + - name: Build ${{ matrix.platform }} ${{ matrix.service }} docker image + uses: docker/build-push-action@v3 + with: + context: . + file: ${{ steps.build-image-info.outputs.dockerFileLocation }} + platforms: ${{ matrix.platform }} + push: true + build-args: | + SERVICE_NAME=${{ matrix.service }} + tags: ${{ steps.build-image-info.outputs.tags }} + - name: Slack Notification For Failure/Cancellation + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + ${{ matrix.platform }} ${{ matrix.service }} + trigger_deployment: + needs: [ push_docker ] + name: Trigger Deployment + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + steps: + - name: Setting ENV variables for deployment + run: | + echo "APP_TAG=${{needs.push_docker.outputs.nextTag}}" >> $GITHUB_ENV + - name: Checkout the target `master` branch of `secret-infrastructure` + uses: actions/checkout@v3 + with: + repository: ice-blockchain/secret-infrastructure + ref: master + token: ${{ secrets.ICE_CI_CD_BOT_GH_PAT }} + path: secret-infrastructure + fetch-depth: 0 + - name: Update [staging] application tag version in helm/freezer/staging/common-values.yaml + uses: mikefarah/yq@master + with: + cmd: | + cd secret-infrastructure + yq e -i '.generic-service-chart.applicationImage.tag = strenv(APP_TAG)' helm/freezer/staging/common-values.yaml + - name: Update [production] application tag version in helm/freezer/production/common-values.yaml + uses: mikefarah/yq@master + with: + cmd: | + cd secret-infrastructure + yq e -i '.generic-service-chart.applicationImage.tag = strenv(APP_TAG)' helm/freezer/production/common-values.yaml + - name: Update [staging] application tag version in helm/freezer-refrigerant/staging/common-values.yaml + uses: mikefarah/yq@master + with: + cmd: | + cd secret-infrastructure + yq e -i '.generic-service-chart.applicationImage.tag = strenv(APP_TAG)' helm/freezer-refrigerant/staging/common-values.yaml + - name: Update [production] application tag version in helm/freezer-refrigerant/production/common-values.yaml + uses: mikefarah/yq@master + with: + cmd: | + cd secret-infrastructure + yq e -i '.generic-service-chart.applicationImage.tag = strenv(APP_TAG)' helm/freezer-refrigerant/production/common-values.yaml + - name: Commit and Push Changes to Application Tag Version + run: | + cd secret-infrastructure + git config user.name "ice CI/CD Bot" + git config user.email ice-cicd-bot@ice.vip + git add helm/freezer/staging/common-values.yaml + git add helm/freezer/production/common-values.yaml + git add helm/freezer-refrigerant/staging/common-values.yaml + git add helm/freezer-refrigerant/production/common-values.yaml + git commit -m "Updated 'freezer' & 'freezer-refrigerant' tag version (${{env.APP_TAG}}) in application helm chart deployment manifests" + git push --set-upstream origin master + - name: Slack Notification For Success + if: ${{ success() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Succeeded, Waiting for Deployment Status Notification... + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':rocket:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + https://staging.r.api.ice.io/tokenomics/r + https://staging.w.api.ice.io/tokenomics/w + - name: Slack Notification For Failure + if: ${{ github.event_name == 'push' && (failure() || cancelled()) }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.BACKEND_SLACK_WEBHOOK }} + SLACK_TITLE: CI Failed> ${{ job.status }} + SLACK_USERNAME: ${{ github.event.repository.name }}-ci-bot + SLACK_ICON: https://avatars.githubusercontent.com/u/102382658?s=400&u=62e73f4cb845c48a07a19e03c6f84b721e40c0a6&v=4 + SLACK_ICON_EMOJI: ':sob:' + SLACK_COLOR: ${{ job.status }} + SLACK_FOOTER: | + https://staging.r.api.ice.io/tokenomics/r + https://staging.w.api.ice.io/tokenomics/w \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3568ffa --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +*.exe +*.BIN +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +*.app +*.bat +*.cgi +*.com +*.gadget +*.jar +*.pif +*.vb +*.wsf +/out +vendor/ +/Godeps +*.iml +*.ipr +/.idea +*.iws +/.vscode +.tmp-* +.env \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..01ea42c --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,546 @@ +# See https://raw.githubusercontent.com/golangci/golangci-lint/master/.golangci.reference.yml +run: + timeout: 5m + allow-parallel-runners: true + skip-dirs: + - src/external_libs + - autogenerated_by_my_lib + skip-files: + - ".*\\.my\\.go$" + - lib/bad.go + +output: + sort-results: true + +linters-settings: + cyclop: + max-complexity: 15 + package-average: 10.0 + + errcheck: + check-type-assertions: true + check-blank: true + exclude-functions: + #- io/ioutil.ReadFile + #- io.Copy(*bytes.Buffer) + #- io.Copy(os.Stdout) + - encoding/json.Marshal + - encoding/json.MarshalIndent + + errchkjson: + check-error-free-encoding: true + report-no-exported: true + + funlen: + lines: 30 + statements: 30 + + gci: + sections: + - Standard + - Default + - prefix(github.com/ice-blockchain) + + gocognit: + min-complexity: 15 + + goconst: + min-occurrences: 2 + numbers: true + min: 2 + max: 2 + + gocritic: + enabled-tags: + - diagnostic + - style + - performance + - experimental + - opinionated + + settings: + captLocal: + paramsOnly: false + elseif: + skipBalanced: false + hugeParam: + sizeThreshold: 32 + nestingReduce: + bodyWidth: 4 + rangeExprCopy: + sizeThreshold: 32 + skipTestFuncs: false + rangeValCopy: + sizeThreshold: 32 + skipTestFuncs: false + tooManyResultsChecker: + maxResults: 3 + truncateCmp: + skipArchDependent: false + underef: + skipRecvDeref: false + unnamedResult: + checkExported: true + + gocyclo: + min-complexity: 15 + + godot: + scope: all + exclude: + - "/v1r" + - "/v1w" + - "^fixme:" + - "^todo:" + - "^TODO:" + - "SPDX-License-Identifier: ice License 1.0" + capital: true + + gofumpt: + extra-rules: true + + goheader: + template: |- + SPDX-License-Identifier: ice License 1.0 + + + goimports: + local-prefixes: github.com/ice-blockchain + + gosec: + exclude-generated: true + config: + global: + nosec: true + show-ignored: true + audit: true + G101: + ignore_entropy: true + entropy_threshold: "80.0" + per_char_threshold: "3.0" + truncate: "32" + + govet: + check-shadowing: true + settings: + printf: + funcs: + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + shadow: + strict: true + enable-all: true + + grouper: + const-require-grouping: true + import-require-single-import: true + import-require-grouping: true + type-require-grouping: true + var-require-grouping: true + + lll: + line-length: 160 + + makezero: + always: true + + nolintlint: + require-explanation: true + require-specific: true + + predeclared: + q: true + + revive: + ignore-generated-header: true + severity: error + enable-all-rules: true + confidence: 0.1 + rules: + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#add-constant + - name: add-constant + severity: error + disabled: true + arguments: + - maxLitCount: "3" + allowStrs: '""' + allowInts: "0,1,2" + allowFloats: "0.0,0.,1.0,1.,2.0,2." + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#datarace + - name: datarace + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any + - name: use-any + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#comment-spacings + - name: comment-spacings + severity: error + disabled: false + arguments: ["nolint"] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#atomic + - name: argument-limit + severity: error + disabled: false + arguments: [ 5 ] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#atomic + - name: atomic + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#banned-characters + - name: banned-characters + severity: error + disabled: true + arguments: ["Ω","Σ","σ", "7"] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bare-return + - name: bare-return + severity: error + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#blank-imports + - name: blank-imports + severity: error + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr + - name: bool-literal-in-expr + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#call-to-gc + - name: call-to-gc + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cognitive-complexity + - name: cognitive-complexity + severity: error + disabled: false + arguments: [ 15 ] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-naming + - name: confusing-naming + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-results + - name: confusing-results + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#constant-logical-expr + - name: constant-logical-expr + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-as-argument + - name: context-as-argument + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-keys-type + - name: context-keys-type + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cyclomatic + - name: cyclomatic + severity: error + disabled: false + arguments: [ 15 ] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#deep-exit + - name: deep-exit + severity: error + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#defer + - name: defer + severity: error + disabled: false + arguments: + - [ "call-chain", "loop", "method-call", "recover", "return" ] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#dot-imports + - name: dot-imports + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#duplicated-imports + - name: duplicated-imports + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return + - name: early-return + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block + - name: empty-block + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines + - name: empty-lines + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-naming + - name: error-naming + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-return + - name: error-return + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-strings + - name: error-strings + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#errorf + - name: errorf + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#exported + - name: exported + severity: error + disabled: false + arguments: + - "checkPrivateReceivers" + - "sayRepetitiveInsteadOfStutters" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#file-header + - name: file-header + severity: error + disabled: false + arguments: + - "SPDX-License-Identifier: ice License 1.0" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#flag-parameter + - name: flag-parameter + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-result-limit + - name: function-result-limit + severity: error + disabled: false + arguments: [ 3 ] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-length + - name: function-length + severity: error + disabled: true + arguments: [ 30, 30 ] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#get-return + - name: get-return + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#identical-branches + - name: identical-branches + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#if-return + - name: if-return + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#increment-decrement + - name: increment-decrement + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#indent-error-flow + - name: indent-error-flow + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blacklist + - name: imports-blacklist + severity: error + disabled: true + arguments: + - "crypto/md5" + - "crypto/sha1" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing + - name: import-shadowing + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit + - name: line-length-limit + severity: error + disabled: true + arguments: [ 160 ] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#max-public-structs + - name: max-public-structs + severity: error + disabled: true + arguments: [ 3 ] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-parameter + - name: modifies-parameter + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-value-receiver + - name: modifies-value-receiver + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#nested-structs + - name: nested-structs + severity: error + disabled: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#optimize-operands-order + - name: optimize-operands-order + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#package-comments + - name: package-comments + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range + - name: range + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-in-closure + - name: range-val-in-closure + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-address + - name: range-val-address + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#receiver-naming + - name: receiver-naming + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redefines-builtin-id + - name: redefines-builtin-id + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-of-int + - name: string-of-int + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format + - name: string-format + severity: error + disabled: true + arguments: + - - 'core.WriteError[1].Message' + - '/^([^A-Z]|$)/' + - must not start with a capital letter + - - 'fmt.Errorf[0]' + - '/(^|[^\.!?])$/' + - must not end in punctuation + - - panic + - '/^[^\n]*$/' + - must not contain line breaks + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag + - name: struct-tag + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#superfluous-else + - name: superfluous-else + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-equal + - name: time-equal + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-naming + - name: time-naming + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-naming + - name: var-naming + severity: error + disabled: false + arguments: + - [ "ID", "URL" ] # AllowList + - [ "VM" ] # DenyList + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-declaration + - name: var-declaration + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unconditional-recursion + - name: unconditional-recursion + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming + - name: unexported-naming + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-return + - name: unexported-return + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error + - name: unhandled-error + severity: error + disabled: false + arguments: + - "fmt.Printf" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unnecessary-stmt + - name: unnecessary-stmt + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unreachable-code + - name: unreachable-code + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter + - name: unused-parameter + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver + - name: unused-receiver + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break + - name: useless-break + severity: error + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#waitgroup-by-value + - name: waitgroup-by-value + severity: error + disabled: false + + rowserrcheck: + packages: + - github.com/jmoiron/sqlx + + tagliatelle: + case: + use-field-name: true + rules: + json: camel + yaml: camel + xml: camel + bson: camel + avro: camel + mapstructure: camel + msgpack: camel + + tenv: + all: true + + varnamelen: + min-name-length: 2 + check-return: true + check-type-param: true + ignore-type-assert-ok: true + ignore-map-index-ok: true + ignore-chan-recv-ok: true + +linters: + disable: + # TODO remove asap! + - gomoddirectives + # The following we don't need at all, ever, in any scenario. + - importas + - durationcheck + - exhaustruct + - promlinter + - nonamedreturns + - ireturn + - gomodguard + - depguard + # The following might be usable in some scenarios. + - wsl + - testpackage + - decorder + # The following are deprecated. + - exhaustivestruct + - golint + - scopelint + - maligned + - interfacer + enable-all: true + disable-all: false + fast: false + +issues: + new: false + max-issues-per-linter: 0 + max-same-issues: 0 + +severity: + default-severity: error diff --git a/LICENSE.header b/LICENSE.header new file mode 100644 index 0000000..79d1d0d --- /dev/null +++ b/LICENSE.header @@ -0,0 +1 @@ +SPDX-License-Identifier: ice License 1.0 \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..a7a43fd --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,50 @@ +ice License + +Version 1.0, January 2023 + +----------------------------------------------------------------------------- + + +Licensor: ice Labs Limited + +Licensed Work: ice Network + The Licensed Work is (c) 2023 ice Labs Limited + +----------------------------------------------------------------------------- + + +Permission is hereby granted by the application Software Developer, ice Labs +Limited, free of charge, to any person obtaining a copy of this application, +software, and associated documentation files (the Software), which was +developed by the Software Developer (ice Labs Limited) for use on ice Network +whereby the purpose of this license is to permit the development of +derivative works based on the Software, including the right to use, copy, +modify, merge, publish, distribute, sub-license, and/or sell copies of such +derivative works and any Software components incorporated therein, and to +permit persons to whom such derivative works are furnished to do so, in each +case, solely to develop, use, and market applications for the official ice +Network. + +All Derivative Works developed under this License for use on the ice Network +may only be released after the official launch of the ice Network’s Mainnet. + +For purposes of this license, ice Network shall mean any application, +software, or another present or future platform developed, owned, or managed +by ice Labs Limited, and its parents, affiliates, or subsidiaries. + +Disclaimer of Warranty. Unless required by applicable law or agreed to in +writing, Licensor provides the Software on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, +without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, +MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely +responsible for determining the appropriateness of using or redistributing +the Software and assume any risks associated with Your exercise of +permissions under this License. + +Limitation of Liability. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cb43b45 --- /dev/null +++ b/Makefile @@ -0,0 +1,256 @@ +.DEFAULT_GOAL := all + +DOCKER_REGISTRY ?= registry.digitalocean.com/ice-io +DOCKER_TAG ?= latest-locally +GO_VERSION_MANIFEST := https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json +REQUIRED_COVERAGE_PERCENT := 0 +COVERAGE_FILE := cover.out +REPOSITORY := $(shell basename `pwd`) + +CGO_ENABLED := 1 +GOOS ?= +GOARCH ?= +SERVICE_NAME ?= +SERVICES := $(wildcard ./cmd/*) + +export CGO_ENABLED GOOS GOARCH SERVICE_NAME + +define getLatestGoPatchVersion + $(shell curl -s $(GO_VERSION_MANIFEST) | jq -r '.[0].version') +endef + +define getLatestGoMinorVersion + $(shell echo $(call getLatestGoPatchVersion) | cut -f1,2 -d'.') +endef + +latestGoVersion: + @echo $(call getLatestGoPatchVersion) + +latestGoMinorVersion: + @echo $(call getLatestGoMinorVersion) + +updateGoModVersion: + go mod edit -go $(call getLatestGoMinorVersion) + +checkModVersion: updateGoModVersion + @if git status --porcelain | grep -q go.mod; then \ + echo "Outdated go version in go.mod. Please update it using 'make updateGoModVersion' and make sure everything works correctly and tests pass then commit the changes."; \ + exit 1; \ + fi; \ + true; + +updateAllDependencies: + go get -t -u ./... + go mod tidy + +checkIfAllDependenciesAreUpToDate: updateAllDependencies + @if git status --porcelain | grep -q go.sum; then \ + echo "Some dependencies are outdated. Please update all dependencies using 'make updateAllDependencies' and make sure everything works correctly and tests pass then commit the changes."; \ + exit 1; \ + fi; \ + true; + +generate-swagger: + swag init --parseDependency --parseInternal -d ${SERVICE} -g $(shell echo "$${SERVICE##*/}" | sed 's/-/_/g').go -o ${SERVICE}/api; + +generate-swaggers: + go install github.com/swaggo/swag/cmd/swag@latest + set -xe; \ + [ -d cmd ] && find ./cmd -mindepth 1 -maxdepth 1 -type d -print | grep -v 'fixture' | sed 's/\.\///g' | while read service; do \ + env SERVICE=$${service} $(MAKE) generate-swagger; \ + done; + +format-swagger: + swag fmt -d ${SERVICE} -g $(shell echo "$${SERVICE##*/}" | sed 's/-/_/g').go + +format-swaggers: + set -xe; \ + [ -d cmd ] && find ./cmd -mindepth 1 -maxdepth 1 -type d -print | grep -v 'fixture' | sed 's/\.\///g' | while read service; do \ + env SERVICE=$${service} $(MAKE) format-swagger; \ + done; + +generate-mocks: +# go install github.com/golang/mock/mockgen@latest +# mockgen -source=CHANGE_ME.go -destination=CHANGE_ME.go -package=CHANGE_ME + +generate: + $(MAKE) generate-swaggers + $(MAKE) format-swaggers + $(MAKE) generate-mocks + $(MAKE) addLicense + $(MAKE) format-imports + +checkGenerated: generate + @if git status --porcelain | grep -e [.]go -e [.]json -e [.]yaml; then \ + echo "Please commit generated files, using 'make generate'."; \ + git --no-pager diff; \ + exit 1; \ + fi; \ + true; + +build-all@ci/cd: + go build -tags=go_json -a -v -race ./... + +build: build-all@ci/cd + +binary-specific-service: + set -xe; \ + echo "$@: $(SERVICE_NAME) / $(GOARCH)" ; \ + go build -tags=go_json -a -v -o ./cmd/$${SERVICE_NAME}/bin ./cmd/$${SERVICE_NAME}; \ + +test: + set -xe; \ + mf="$$(pwd)/Makefile"; \ + find . -mindepth 1 -maxdepth 4 -type d -print | grep -v '\./\.' | grep -v '/\.' | sed 's/\.\///g' | while read service; do \ + cd $${service} ; \ + if [[ $$(find . -mindepth 1 -maxdepth 1 -type f -print | grep -E '_test.go' | wc -l | sed "s/ //g") -gt 0 ]]; then \ + make -f $$mf test@ci/cd; \ + fi ; \ + for ((i=0;i<$$(echo "$${service}" | grep -o "/" | wc -l | sed "s/ //g");i++)); do \ + cd .. ; \ + done; \ + cd .. ; \ + done; + +# TODO should be improved to a per file check and maybe against a previous value +#(maybe we should use something like SonarQube for this?) +coverage: $(COVERAGE_FILE) + @t=`go tool cover -func=$(COVERAGE_FILE) | grep total | grep -Eo '[0-9]+\.[0-9]+'`;\ + echo "Total coverage: $${t}%"; \ + if [ "$${t%.*}" -lt $(REQUIRED_COVERAGE_PERCENT) ]; then \ + echo "ERROR: It has to be at least $(REQUIRED_COVERAGE_PERCENT)%"; \ + exit 1; \ + fi; + +test@ci/cd: + # TODO make -race work + go test -timeout 20m -tags=go_json,test -v -cover -coverprofile=$(COVERAGE_FILE) -covermode atomic + +benchmark@ci/cd: + # TODO make -race work + go test -timeout 20m -tags=go_json,test -run=^$ -v -bench=. -benchmem -benchtime 10s + +benchmark: + set -xe; \ + mf="$$(pwd)/Makefile"; \ + find . -mindepth 1 -maxdepth 4 -type d -print | grep -v '\./\.' | grep -v '/\.' | sed 's/\.\///g' | while read service; do \ + cd $${service} ; \ + if [[ $$(find . -mindepth 1 -maxdepth 1 -type f -print | grep -E '_test.go' | wc -l | sed "s/ //g") -gt 0 ]]; then \ + make -f $$mf benchmark@ci/cd; \ + fi ; \ + for ((i=0;i<$$(echo "$${service}" | grep -o "/" | wc -l | sed "s/ //g");i++)); do \ + cd .. ; \ + done; \ + cd .. ; \ + done; + +print-all-packages-with-tests: + set -xe; \ + find . -mindepth 1 -maxdepth 4 -type d -print | grep -v '\./\.' | grep -v '/\.' | sed 's/\.\///g' | while read service; do \ + cd $${service} ; \ + if [[ $$(find . -mindepth 1 -maxdepth 1 -type f -print | grep -E '_test.go' | wc -l | sed "s/ //g") -gt 0 ]]; then \ + echo "$${service}"; \ + fi ; \ + for ((i=0;i<$$(echo "$${service}" | grep -o "/" | wc -l | sed "s/ //g");i++)); do \ + cd .. ; \ + done; \ + cd .. ; \ + done; + +clean: + @go clean + @rm -f tmp$(COVERAGE_FILE) $(COVERAGE_FILE) 2>/dev/null || true + @test -d cmd && find ./cmd -mindepth 2 -maxdepth 2 -type f -name bin -exec rm -f {} \; || true; + @test -d cmd && find ./cmd -mindepth 2 -maxdepth 2 -type d -name bins -exec rm -Rf {} \; || true; + @find . -name ".tmp-*" -exec rm -Rf {} \; || true; + @find . -mindepth 1 -maxdepth 3 -type f -name $(COVERAGE_FILE) -exec rm -Rf {} \; || true; + @find . -mindepth 1 -maxdepth 3 -type f -name tmp$(COVERAGE_FILE) -exec rm -Rf {} \; || true; + +lint: + golangci-lint run + +# run specific service by its name +run-%: + go run -tags=go_json -v ./cmd/$* + +run: +ifeq ($(words $(SERVICES)),1) + $(MAKE) $(subst ./cmd/,run-,$(SERVICES)) +else + @echo "Do not know what to run" + @echo "Targets:" + @for target in $(subst ./cmd/,run-,$(SERVICES)); do \ + echo " $${target}"; \ + done; false; +endif + +# run specific service by its name +binary-run-%: + ./cmd/$*/bin + +binary-run: +ifeq ($(words $(SERVICES)),1) + $(MAKE) $(subst ./cmd/,binary-run-,$(SERVICES)) +else + @echo "Do not know what to run" + @echo "Targets:" + @for target in $(subst ./cmd/,binary-run-,$(SERVICES)); do \ + echo " $${target}"; \ + done; false; +endif + +# note: it requires make-4.3+ to run that +buildMultiPlatformDockerImage: + set -xe; \ + find ./cmd -mindepth 1 -maxdepth 1 -type d -print | grep -v 'fixture' | while read service; do \ + for arch in amd64 arm64 s390x ppc64le; do \ + docker buildx build \ + --platform linux/$${arch} \ + -f $${service}/Dockerfile \ + --label os=linux \ + --label arch=$${arch} \ + --force-rm \ + --pull -t $(DOCKER_REGISTRY)/$(REPOSITORY)/$${service##*/}:$(DOCKER_TAG) \ + --build-arg SERVICE_NAME=$${service##*/} \ + --build-arg TARGETARCH=$${arch} \ + --build-arg TARGETOS=linux \ + .; \ + done; \ + done; + +start-test-environment: + #go run -race -v local.go + go run -v local.go --type all + +start-test-environment-%: + #go run -race -v local.go + go run -v local.go --type $* + +getAddLicense: + GO111MODULE=off go get -v -u github.com/google/addlicense + +addLicense: getAddLicense + `go env GOPATH`/bin/addlicense -f LICENSE.header * .github/* + +checkLicense: getAddLicense + `go env GOPATH`/bin/addlicense -f LICENSE.header -check * .github/* + +fix-field-alignment: + go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest + fieldalignment -fix ./... + +format-imports: + go install golang.org/x/tools/cmd/goimports@latest + go install github.com/daixiang0/gci@latest + gci write -s standard -s default -s "prefix(github.com/ice-blockchain)" ./.. + goimports -w -local github.com/ice-blockchain ./.. + +print-token-%: + go run -v local.go --generateAuth $* + +start-seeding: + go run -v local.go --startSeeding true + +all: checkLicense checkModVersion checkIfAllDependenciesAreUpToDate checkGenerated build test coverage benchmark clean +local: addLicense updateGoModVersion updateAllDependencies generate build buildMultiPlatformDockerImage test coverage benchmark lint clean +dockerfile: binary-specific-service diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a650d9 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Freezer Service + +``Freezer is handling everything related to user's ice tokenomics.`` + +### Development + +These are the crucial/critical operations you will need when developing `Freezer`: + +1. If you need to generate a new Authorization Token & UserID for testing locally: + 1. run `make print-token-XXX`, where `XXX` is the role you want for the user. +2. If you need to seed your local database, or even a remote one: + 1. run `make start-seeding` + 2. it requires an .env entry: `MASTER_DB_INSTANCE_ADDRESS=admin:pass@127.0.0.1:3301` +3. `make run-freezer` + 1. This runs the actual read service. + 2. It will feed off of the properties in `./application.yaml` + 3. By default, https://localhost:2443/tokenomics/r runs the Open API (Swagger) entrypoint. +4. `make run-freezer-refrigerant` + 1. This runs the actual write service. + 2. It will feed off of the properties in `./application.yaml` + 3. By default, https://localhost:3443/tokenomics/w runs the Open API (Swagger) entrypoint. +5. `make start-test-environment` + 1. This bootstraps a local test environment with **Freezer**'s dependencies using your `docker` and `docker-compose` daemons. + 2. It is a blocking operation, SIGTERM or SIGINT will kill it. + 3. It will feed off of the properties in `./application.yaml` + 1. MessageBroker GUIs + 1. https://www.conduktor.io + 2. https://www.kafkatool.com + 3. (CLI) https://vectorized.io/redpanda + 2. DB GUIs + 1. https://github.com/tarantool/awesome-tarantool#gui-clients + 2. (CLI) `docker exec -t -i mytarantool console` where `mytarantool` is the container name +6. `make all` + 1. This runs the CI pipeline, locally -- the same pipeline that PR checks run. + 2. Run it before you commit to save time & not wait for PR check to fail remotely. +7. `make local` + 1. This runs the CI pipeline, in a descriptive/debug mode. Run it before you run the "real" one. +8. `make lint` + 1. This runs the linters. It is a part of the other pipelines, so you can run this separately to fix lint issues. +9. `make test` + 1. This runs all tests. +10. `make benchmark` + 1. This runs all benchmarks. diff --git a/application.yaml b/application.yaml new file mode 100644 index 0000000..88c3db9 --- /dev/null +++ b/application.yaml @@ -0,0 +1,189 @@ +# SPDX-License-Identifier: ice License 1.0 + +development: true +logger: + encoder: console + level: info +cmd/freezer: + host: localhost:2443 + version: local + defaultEndpointTimeout: 30s + httpServer: + port: 2443 + certPath: cmd/freezer/.testdata/localhost.crt + keyPath: cmd/freezer/.testdata/localhost.key + defaultPagination: + limit: 20 + maxLimit: 1000 +cmd/freezer-refrigerant: + host: localhost:3443 + version: local + defaultEndpointTimeout: 30s + httpServer: + port: 3443 + certPath: cmd/freezer-refrigerant/.testdata/localhost.crt + keyPath: cmd/freezer-refrigerant/.testdata/localhost.key + defaultPagination: + limit: 20 + maxLimit: 1000 +tokenomics: &tokenomics + db: &tokenomicsDatabase + urls: + - localhost:3401 + user: admin + password: pass + messageBroker: &tokenomicsMessageBroker + consumerGroup: freezer-local + createTopics: true + urls: + - localhost:9092 + topics: &tokenomicsMessageBrokerTopics + - name: freezer-health-check + partitions: 1 + replicationFactor: 1 + retention: 10s + - name: adoption-table + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: mining-sessions-table + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: mining-rates-table + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: balances-table + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: add-balance-commands + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: pre-stakings-table + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: available-daily-bonuses + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: started-days-off + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: balance-recalculation-trigger-stream + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: mining-rates-recalculation-trigger-stream + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: blockchain-balance-synchronization-trigger-stream + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: extra-bonus-processing-trigger-stream + partitions: 10 + replicationFactor: 1 + retention: 10s + ### The next topics are not owned by this service, but are needed to be created for the local/test environment. + - name: users-table + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: global-table + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: viewed-news + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: user-device-metadata-table + partitions: 10 + replicationFactor: 1 + retention: 10s + consumingTopics: + - name: users-table + - name: global-table + - name: mining-sessions-table + - name: add-balance-commands + - name: viewed-news + - name: user-device-metadata-table + - name: balance-recalculation-trigger-stream + oneGoroutinePerPartition: true + - name: mining-rates-recalculation-trigger-stream + oneGoroutinePerPartition: true + - name: blockchain-balance-synchronization-trigger-stream + oneGoroutinePerPartition: true + - name: extra-bonus-processing-trigger-stream + oneGoroutinePerPartition: true + wintr/multimedia/picture: + urlDownload: https://ice-staging.b-cdn.net/profile + workerCount: 10 + referralBonusMiningRates: + t0: 25 + t1: 25 + t2: 5 + rollbackNegativeMining: + aggressiveDegradationStartsAfter: 30m + lastXMiningSessionsCollectingInterval: 1m + available: + after: 5m + until: 60m + miningSessionDuration: + min: 30s + max: 1m + warnAboutExpirationAfter: 50s + consecutiveNaturalMiningSessionsRequiredFor1ExtraFreeArtificialMiningSession: + min: 12 + max: 6 + globalAggregationInterval: + parent: 60m + child: 1m + adoptionMilestoneSwitch: + duration: 60s + consecutiveDurationsRequired: 7 + activeUserMilestones: + - 2 + - 4 + - 6 + - 8 + - 10 + extraBonuses: + duration: 24m + utcOffsetDuration: 6s + claimWindow: 1m + delayedClaimPenaltyWindow: 15s + availabilityWindow: 10m + timeToAvailabilityWindow: 10m + flatValues: + - 2 + - 4 + - 6 + - 8 + - 10 + newsSeenValues: + - 0 + - 6 + - 15 + - 54 + - 90 + miningStreakValues: + - 0 + - 2 + - 5 + - 9 + - 20 +tokenomics_test: + <<: *tokenomics + messageBroker: + <<: *tokenomicsMessageBroker + consumingTopics: *tokenomicsMessageBrokerTopics + consumerGroup: freezer-local-test + db: + <<: *tokenomicsDatabase diff --git a/cmd/freezer-refrigerant/.testdata/application.yaml b/cmd/freezer-refrigerant/.testdata/application.yaml new file mode 100644 index 0000000..111ae16 --- /dev/null +++ b/cmd/freezer-refrigerant/.testdata/application.yaml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: ice License 1.0 + +development: false +logger: + encoder: console + level: info +cmd/freezer-refrigerant: + host: localhost + version: latest + defaultEndpointTimeout: 5s + httpServer: + port: 34443 + certPath: .testdata/localhost.crt + keyPath: .testdata/localhost.key + defaultPagination: + limit: 20 + maxLimit: 1000 +#TODO \ No newline at end of file diff --git a/cmd/freezer-refrigerant/.testdata/expected_swagger.json b/cmd/freezer-refrigerant/.testdata/expected_swagger.json new file mode 100644 index 0000000..e69de29 diff --git a/cmd/freezer-refrigerant/.testdata/localhost.crt b/cmd/freezer-refrigerant/.testdata/localhost.crt new file mode 100644 index 0000000..84c6603 --- /dev/null +++ b/cmd/freezer-refrigerant/.testdata/localhost.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDijCCAnKgAwIBAgIJAMeawIdSd6+8MA0GCSqGSIb3DQEBCwUAMCcxCzAJBgNV +BAYTAlVTMRgwFgYDVQQDDA9FeGFtcGxlLVJvb3QtQ0EwHhcNMjIwMTA0MjEwNDE3 +WhcNMjQxMDI0MjEwNDE3WjBtMQswCQYDVQQGEwJVUzESMBAGA1UECAwJWW91clN0 +YXRlMREwDwYDVQQHDAhZb3VyQ2l0eTEdMBsGA1UECgwURXhhbXBsZS1DZXJ0aWZp +Y2F0ZXMxGDAWBgNVBAMMD2xvY2FsaG9zdC5sb2NhbDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAONuA1zntIXbNaEvt/n+/Jisib/8Bjvfm2I9ENMq0TBH +OGlbZgJ9ywiKsrxBYH/O2q6Dsxy9fL5cSfcMmAS0FXPrcXQx/pVNCgNWLEXZyPDk +SzSR+tlPXzuryN2/jbWtgOZc73kfxQVBqUWbLyMiXaxMxVGHgpYMg0w68Ee62d2H +AnA7c0YBllvggDRSaoDRJJZTc8DDGAHm9x5583zdxpCQh/EeV+zIjd2lAGF0ioYu +PV69lwyrTnY/s7WG59nRYwYR50JvbI4G+5bbpf4q2W7Q0BVLqwSdMJfAfG43N5U/ +4Q1dfyJeXavFfQaZWJtEiVOU9TBiV3QQto0tI28R6J0CAwEAAaNzMHEwQQYDVR0j +BDowOKErpCkwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1D +QYIJANxKhfP/dJTMMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuC +CWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAjrUp0epptzaaTULvhrFdNJ6e +2WAeJpYCxMXjms7P+B/ldyIirDqG/WEzpN64Z1gXJhtxnw7IGTsQ8eXqLmBDk045 +vHhVbRhjVGADc+EVwX6OzQ+WQEGZzNDPX7DBObLC1ZV5LcfUwQXyACmlARlYgXJN +GZFDkijDcvY3/Hyq9NkV6VGYPKnzxaal3v3cYO8FXQHaOLnu+SLWknT56y2vTa5/ +H4CoX8nrts5Fa0NuOdoyNA1c7IdHjR/dy4g5IUZW+Sbhr1nNgkECBJvJ5QOWZ3M4 +4a8NroD0ikzQDaeS4Tpk54WnJLEjDgQe5fX9RMu9F2sbr+wP+gUTmHuhLg/Ptw== +-----END CERTIFICATE----- diff --git a/cmd/freezer-refrigerant/.testdata/localhost.key b/cmd/freezer-refrigerant/.testdata/localhost.key new file mode 100644 index 0000000..f2fa28b --- /dev/null +++ b/cmd/freezer-refrigerant/.testdata/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDjbgNc57SF2zWh +L7f5/vyYrIm//AY735tiPRDTKtEwRzhpW2YCfcsIirK8QWB/ztqug7McvXy+XEn3 +DJgEtBVz63F0Mf6VTQoDVixF2cjw5Es0kfrZT187q8jdv421rYDmXO95H8UFQalF +my8jIl2sTMVRh4KWDINMOvBHutndhwJwO3NGAZZb4IA0UmqA0SSWU3PAwxgB5vce +efN83caQkIfxHlfsyI3dpQBhdIqGLj1evZcMq052P7O1hufZ0WMGEedCb2yOBvuW +26X+Ktlu0NAVS6sEnTCXwHxuNzeVP+ENXX8iXl2rxX0GmVibRIlTlPUwYld0ELaN +LSNvEeidAgMBAAECggEBALHtN6RPgePXA7X+5ygmXOf01C/ms9nTrnTE4YzTSqVC +kteaMcxxLY6ZNAwj+aMD6gHt9wrdE+K5wQQOTkAfw0jVQgVtt4aGpvbFTA25vIL5 +l/yg2Gd6uT6tvo/9dJhWDSosOw2/1RuvqwZRyibqk+5ggV6vbXKGh5Hz6lezzw6H +P8xazcT634Tj5YhNhd00XIcr1V+kqEHZGiJP0XzrdXzjAS5NciEdW529gv4Dp4Ni +zpSroznCcP6psLXS99snDg1UdQPFu90IW51i7VOBkF+RhRIMWOywO9FeFHoQ7j0u +SqACHFz8HQnR0uSZ8AwnWrRhWVoBfQ6bwDjJKi/vtQECgYEA8ZxQtliNEd2ojF0s +PbU7YE9vTDEY5AXk6bRPf1rJk/RTDZZwguC4MWjTBpcqawppzur8RLRJAp3WtyP4 +zXh7qvgeOFIaVmGUefEfg8OkXAtvwT+ogvl9HHyY3lPWQyF+WV3dN4ILWguDYiCB +myL/4EqBZjSHmqfzKS6sT9x+TYkCgYEA8Pl9uH6wDSReKqmO1kNnyF+dWfP0I7wX +UpSTkRvSrYQIH2VFYH+LSN5OZaku0FHQkIbgjunAT29N8p//E2ZA3L2xNIKDV+hI +M+NV52YwguUROh2mIypGlPT1f7R+tiYzz27jZgctYIF3mzTMQ1TC2TqgXzG5eA2y +/Ojcmj9ncXUCgYEA4y5fOkYjR3RMAsetTMy3awTmGxdjVy0vpIx138NHHYaz/WfC +nV2d9F+jZWQIb6PX/8c2s4dtyzcM6SG61cD/T7CEAeM5fpW8XbjbMDNqvV3HlEc+ +NQFQodOKjir4oiDBRFidJI90CxQeUstL8srDHGwSJj8obsSTQNrxDRq/7DkCgYBR +cLBpmv9a4bClkHqCtXMsyAvA6+7V6Oqk8SvSPen81IN+QNaqn1BuhxtNxljY9N2d +Csh35E4nSoG4fxRQ9Rz0vXNXQMis/Aby6mEM/H9mrY4d6wlMFyyViRgzWcf9PXoD +IAHgaIqQdBD9NmHWW54ilmq+4WpCRbb5PKXZx5XpRQKBgQCCMpaANqren/4aeDdz +F2lkEJweRsTaS13LJKkk/fGWeXo3N/sXuBPocViSzkCNoHGx1yHrG9TyC7Cz7UXj +4Dpy7gI3cg0i7gaHgC1JfYoPzCSmvnJT62TyL/5SGwF4Xkg8efmF+sVKZqsqgiiT +ATGyCMbfg4XaTw84ubV2rGxvRQ== +-----END PRIVATE KEY----- diff --git a/cmd/freezer-refrigerant/Dockerfile b/cmd/freezer-refrigerant/Dockerfile new file mode 100644 index 0000000..f913afd --- /dev/null +++ b/cmd/freezer-refrigerant/Dockerfile @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: ice License 1.0 + +FROM golang:latest AS build +ARG SERVICE_NAME +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /app/ +COPY . /app/ + +ENV CGO_ENABLED=1 +ENV GOOS=$TARGETOS +ENV GOARCH=$TARGETARCH + +RUN env SERVICE_NAME=$SERVICE_NAME make dockerfile +RUN cp cmd/$SERVICE_NAME/bin bin + +FROM gcr.io/distroless/base-debian11:latest +ARG TARGETOS +ARG TARGETARCH +ARG PORT=443 +LABEL os=$TARGETOS +LABEL arch=$TARGETARCH +COPY --from=build /app/bin app +#You might need to expose more ports. Just add more separated by space +#I.E. EXPOSE 8080 8081 8082 8083 +EXPOSE $PORT +ENTRYPOINT ["/app"] diff --git a/cmd/freezer-refrigerant/api/docs.go b/cmd/freezer-refrigerant/api/docs.go new file mode 100644 index 0000000..8081438 --- /dev/null +++ b/cmd/freezer-refrigerant/api/docs.go @@ -0,0 +1,506 @@ +// Code generated by swaggo/swag. DO NOT EDIT. + +package api + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "ice.io", + "url": "https://ice.io" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/tokenomics/{userId}/extra-bonus-claims": { + "post": { + "description": "Claims an extra bonus for the user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/tokenomics.ExtraBonusSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if user not found or no extra bonus available", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "409": { + "description": "if already claimed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/mining-sessions": { + "post": { + "description": "Starts a new mining session for the user, if not already in progress with another one.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "description": "Request params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.StartNewMiningSessionRequestBody" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/tokenomics.MiningSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if user not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "409": { + "description": "if mining is in progress or if a decision about negative mining progress is required", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/pre-staking": { + "put": { + "description": "Starts or updates pre-staking for the user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "description": "Request params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.StartOrUpdatePreStakingRequestBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.PreStakingSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "user not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "main.StartNewMiningSessionRequestBody": { + "type": "object", + "properties": { + "resurrect": { + "description": "Specify this if you want to resurrect the user.\n` + "`" + `true` + "`" + ` recovers all the lost balance, ` + "`" + `false` + "`" + ` deletes it forever, ` + "`" + `null/undefined` + "`" + ` does nothing. Default is ` + "`" + `null/undefined` + "`" + `.", + "type": "boolean", + "example": true + } + } + }, + "main.StartOrUpdatePreStakingRequestBody": { + "type": "object", + "properties": { + "allocation": { + "type": "integer", + "maximum": 100, + "example": 100 + }, + "years": { + "type": "integer", + "maximum": 5, + "example": 1 + } + } + }, + "server.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "SOMETHING_NOT_FOUND" + }, + "data": { + "type": "object", + "additionalProperties": {} + }, + "error": { + "type": "string", + "example": "something is missing" + } + } + }, + "tokenomics.ExtraBonusSummary": { + "type": "object", + "properties": { + "availableExtraBonus": { + "type": "integer", + "example": 2 + } + } + }, + "tokenomics.MiningRateBonuses": { + "type": "object", + "properties": { + "extra": { + "type": "integer", + "example": 300 + }, + "preStaking": { + "type": "integer", + "example": 300 + }, + "t1": { + "type": "integer", + "example": 100 + }, + "t2": { + "type": "integer", + "example": 200 + }, + "total": { + "type": "integer", + "example": 300 + } + } + }, + "tokenomics.MiningRateSummary-coin_ICE": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "example": "1,234,232.001" + }, + "bonuses": { + "$ref": "#/definitions/tokenomics.MiningRateBonuses" + } + } + }, + "tokenomics.MiningRateType": { + "type": "string", + "enum": [ + "positive", + "negative", + "none" + ], + "x-enum-varnames": [ + "PositiveMiningRateType", + "NegativeMiningRateType", + "NoneMiningRateType" + ] + }, + "tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "positiveTotalNoPreStakingBonus": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "preStaking": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "standard": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "total": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "totalNoPreStakingBonus": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "type": { + "$ref": "#/definitions/tokenomics.MiningRateType" + } + } + }, + "tokenomics.MiningSession": { + "type": "object", + "properties": { + "endedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "free": { + "type": "boolean", + "example": true + }, + "resettableStartingAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "startedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "warnAboutExpirationStartingAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + } + } + }, + "tokenomics.MiningSummary": { + "type": "object", + "properties": { + "availableExtraBonus": { + "type": "integer", + "example": 2 + }, + "miningRates": { + "$ref": "#/definitions/tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE" + }, + "miningSession": { + "$ref": "#/definitions/tokenomics.MiningSession" + }, + "miningStreak": { + "type": "integer", + "example": 2 + }, + "remainingFreeMiningSessions": { + "type": "integer", + "example": 1 + } + } + }, + "tokenomics.PreStakingSummary": { + "type": "object", + "properties": { + "allocation": { + "type": "integer", + "example": 100 + }, + "bonus": { + "type": "integer", + "example": 100 + }, + "years": { + "type": "integer", + "example": 1 + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "latest", + Host: "", + BasePath: "/v1w", + Schemes: []string{"https"}, + Title: "Tokenomics API", + Description: "API that handles everything related to write-only operations for user's tokenomics.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/cmd/freezer-refrigerant/api/swagger.json b/cmd/freezer-refrigerant/api/swagger.json new file mode 100644 index 0000000..5e34755 --- /dev/null +++ b/cmd/freezer-refrigerant/api/swagger.json @@ -0,0 +1,485 @@ +{ + "schemes": [ + "https" + ], + "swagger": "2.0", + "info": { + "description": "API that handles everything related to write-only operations for user's tokenomics.", + "title": "Tokenomics API", + "contact": { + "name": "ice.io", + "url": "https://ice.io" + }, + "version": "latest" + }, + "basePath": "/v1w", + "paths": { + "/tokenomics/{userId}/extra-bonus-claims": { + "post": { + "description": "Claims an extra bonus for the user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/tokenomics.ExtraBonusSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if user not found or no extra bonus available", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "409": { + "description": "if already claimed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/mining-sessions": { + "post": { + "description": "Starts a new mining session for the user, if not already in progress with another one.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "description": "Request params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.StartNewMiningSessionRequestBody" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/tokenomics.MiningSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if user not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "409": { + "description": "if mining is in progress or if a decision about negative mining progress is required", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/pre-staking": { + "put": { + "description": "Starts or updates pre-staking for the user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "description": "Request params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.StartOrUpdatePreStakingRequestBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.PreStakingSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "user not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "main.StartNewMiningSessionRequestBody": { + "type": "object", + "properties": { + "resurrect": { + "description": "Specify this if you want to resurrect the user.\n`true` recovers all the lost balance, `false` deletes it forever, `null/undefined` does nothing. Default is `null/undefined`.", + "type": "boolean", + "example": true + } + } + }, + "main.StartOrUpdatePreStakingRequestBody": { + "type": "object", + "properties": { + "allocation": { + "type": "integer", + "maximum": 100, + "example": 100 + }, + "years": { + "type": "integer", + "maximum": 5, + "example": 1 + } + } + }, + "server.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "SOMETHING_NOT_FOUND" + }, + "data": { + "type": "object", + "additionalProperties": {} + }, + "error": { + "type": "string", + "example": "something is missing" + } + } + }, + "tokenomics.ExtraBonusSummary": { + "type": "object", + "properties": { + "availableExtraBonus": { + "type": "integer", + "example": 2 + } + } + }, + "tokenomics.MiningRateBonuses": { + "type": "object", + "properties": { + "extra": { + "type": "integer", + "example": 300 + }, + "preStaking": { + "type": "integer", + "example": 300 + }, + "t1": { + "type": "integer", + "example": 100 + }, + "t2": { + "type": "integer", + "example": 200 + }, + "total": { + "type": "integer", + "example": 300 + } + } + }, + "tokenomics.MiningRateSummary-coin_ICE": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "example": "1,234,232.001" + }, + "bonuses": { + "$ref": "#/definitions/tokenomics.MiningRateBonuses" + } + } + }, + "tokenomics.MiningRateType": { + "type": "string", + "enum": [ + "positive", + "negative", + "none" + ], + "x-enum-varnames": [ + "PositiveMiningRateType", + "NegativeMiningRateType", + "NoneMiningRateType" + ] + }, + "tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "positiveTotalNoPreStakingBonus": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "preStaking": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "standard": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "total": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "totalNoPreStakingBonus": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "type": { + "$ref": "#/definitions/tokenomics.MiningRateType" + } + } + }, + "tokenomics.MiningSession": { + "type": "object", + "properties": { + "endedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "free": { + "type": "boolean", + "example": true + }, + "resettableStartingAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "startedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "warnAboutExpirationStartingAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + } + } + }, + "tokenomics.MiningSummary": { + "type": "object", + "properties": { + "availableExtraBonus": { + "type": "integer", + "example": 2 + }, + "miningRates": { + "$ref": "#/definitions/tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE" + }, + "miningSession": { + "$ref": "#/definitions/tokenomics.MiningSession" + }, + "miningStreak": { + "type": "integer", + "example": 2 + }, + "remainingFreeMiningSessions": { + "type": "integer", + "example": 1 + } + } + }, + "tokenomics.PreStakingSummary": { + "type": "object", + "properties": { + "allocation": { + "type": "integer", + "example": 100 + }, + "bonus": { + "type": "integer", + "example": 100 + }, + "years": { + "type": "integer", + "example": 1 + } + } + } + } +} \ No newline at end of file diff --git a/cmd/freezer-refrigerant/api/swagger.yaml b/cmd/freezer-refrigerant/api/swagger.yaml new file mode 100644 index 0000000..09be244 --- /dev/null +++ b/cmd/freezer-refrigerant/api/swagger.yaml @@ -0,0 +1,337 @@ +# SPDX-License-Identifier: ice License 1.0 + +basePath: /v1w +definitions: + main.StartNewMiningSessionRequestBody: + properties: + resurrect: + description: |- + Specify this if you want to resurrect the user. + `true` recovers all the lost balance, `false` deletes it forever, `null/undefined` does nothing. Default is `null/undefined`. + example: true + type: boolean + type: object + main.StartOrUpdatePreStakingRequestBody: + properties: + allocation: + example: 100 + maximum: 100 + type: integer + years: + example: 1 + maximum: 5 + type: integer + type: object + server.ErrorResponse: + properties: + code: + example: SOMETHING_NOT_FOUND + type: string + data: + additionalProperties: {} + type: object + error: + example: something is missing + type: string + type: object + tokenomics.ExtraBonusSummary: + properties: + availableExtraBonus: + example: 2 + type: integer + type: object + tokenomics.MiningRateBonuses: + properties: + extra: + example: 300 + type: integer + preStaking: + example: 300 + type: integer + t1: + example: 100 + type: integer + t2: + example: 200 + type: integer + total: + example: 300 + type: integer + type: object + tokenomics.MiningRateSummary-coin_ICE: + properties: + amount: + example: 1,234,232.001 + type: string + bonuses: + $ref: '#/definitions/tokenomics.MiningRateBonuses' + type: object + tokenomics.MiningRateType: + enum: + - positive + - negative + - none + type: string + x-enum-varnames: + - PositiveMiningRateType + - NegativeMiningRateType + - NoneMiningRateType + tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE: + properties: + base: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + positiveTotalNoPreStakingBonus: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + preStaking: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + standard: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + total: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + totalNoPreStakingBonus: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + type: + $ref: '#/definitions/tokenomics.MiningRateType' + type: object + tokenomics.MiningSession: + properties: + endedAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + free: + example: true + type: boolean + resettableStartingAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + startedAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + warnAboutExpirationStartingAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + type: object + tokenomics.MiningSummary: + properties: + availableExtraBonus: + example: 2 + type: integer + miningRates: + $ref: '#/definitions/tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE' + miningSession: + $ref: '#/definitions/tokenomics.MiningSession' + miningStreak: + example: 2 + type: integer + remainingFreeMiningSessions: + example: 1 + type: integer + type: object + tokenomics.PreStakingSummary: + properties: + allocation: + example: 100 + type: integer + bonus: + example: 100 + type: integer + years: + example: 1 + type: integer + type: object +info: + contact: + name: ice.io + url: https://ice.io + description: API that handles everything related to write-only operations for user's + tokenomics. + title: Tokenomics API + version: latest +paths: + /tokenomics/{userId}/extra-bonus-claims: + post: + consumes: + - application/json + description: Claims an extra bonus for the user. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/tokenomics.ExtraBonusSummary' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if not allowed + schema: + $ref: '#/definitions/server.ErrorResponse' + "404": + description: if user not found or no extra bonus available + schema: + $ref: '#/definitions/server.ErrorResponse' + "409": + description: if already claimed + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Tokenomics + /tokenomics/{userId}/mining-sessions: + post: + consumes: + - application/json + description: Starts a new mining session for the user, if not already in progress + with another one. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + - description: Request params + in: body + name: request + required: true + schema: + $ref: '#/definitions/main.StartNewMiningSessionRequestBody' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/tokenomics.MiningSummary' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if not allowed + schema: + $ref: '#/definitions/server.ErrorResponse' + "404": + description: if user not found + schema: + $ref: '#/definitions/server.ErrorResponse' + "409": + description: if mining is in progress or if a decision about negative mining + progress is required + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Tokenomics + /tokenomics/{userId}/pre-staking: + put: + consumes: + - application/json + description: Starts or updates pre-staking for the user. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + - description: Request params + in: body + name: request + required: true + schema: + $ref: '#/definitions/main.StartOrUpdatePreStakingRequestBody' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tokenomics.PreStakingSummary' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if not allowed + schema: + $ref: '#/definitions/server.ErrorResponse' + "404": + description: user not found + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Tokenomics +schemes: +- https +swagger: "2.0" diff --git a/cmd/freezer-refrigerant/contract.go b/cmd/freezer-refrigerant/contract.go new file mode 100644 index 0000000..ca2d64e --- /dev/null +++ b/cmd/freezer-refrigerant/contract.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: ice License 1.0 + +package main + +import ( + "github.com/ice-blockchain/freezer/tokenomics" +) + +// Public API. + +type ( + StartNewMiningSessionRequestBody struct { + // Specify this if you want to resurrect the user. + // `true` recovers all the lost balance, `false` deletes it forever, `null/undefined` does nothing. Default is `null/undefined`. + Resurrect *bool `json:"resurrect" example:"true"` + UserID string `uri:"userId" swaggerignore:"true" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + } + ClaimExtraBonusRequestBody struct { + UserID string `uri:"userId" swaggerignore:"true" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + } + StartOrUpdatePreStakingRequestBody struct { + UserID string `uri:"userId" swaggerignore:"true" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + Years uint8 `json:"years" required:"true" maximum:"5" example:"1"` + Allocation uint8 `json:"allocation" required:"true" maximum:"100" example:"100"` + } +) + +// Private API. + +const ( + applicationYamlKey = "cmd/freezer-refrigerant" + swaggerRoot = "/tokenomics/w" +) + +// Values for server.ErrorResponse#Code. +const ( + userNotFoundErrorCode = "USER_NOT_FOUND" + decreasingPreStakingAllocationOrYearsNotAllowedErrorCode = "DECREASING_PRE_STAKING_ALLOCATION_OR_YEARS_NOT_ALLOWED" + miningInProgressErrorCode = "MINING_IN_PROGRESS" + raceConditionErrorCode = "RACE_CONDITION" + resurrectionDecisionRequiredErrorCode = "RESURRECTION_DECISION_REQUIRED" + noExtraBonusAvailableErrorCode = "NO_EXTRA_BONUS_AVAILABLE" + extraBonusAlreadyClaimedErrorCode = "EXTRA_BONUS_ALREADY_CLAIMED" +) + +type ( + // | service implements server.State and is responsible for managing the state and lifecycle of the package. + service struct { + tokenomicsProcessor tokenomics.Processor + } + config struct { + Host string `yaml:"host"` + Version string `yaml:"version"` + } +) diff --git a/cmd/freezer-refrigerant/freezer_refrigerant.go b/cmd/freezer-refrigerant/freezer_refrigerant.go new file mode 100644 index 0000000..3003244 --- /dev/null +++ b/cmd/freezer-refrigerant/freezer_refrigerant.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: ice License 1.0 + +package main + +import ( + "context" + "strconv" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/freezer/cmd/freezer-refrigerant/api" + "github.com/ice-blockchain/freezer/tokenomics" + appCfg "github.com/ice-blockchain/wintr/config" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/server" +) + +// @title Tokenomics API +// @version latest +// @description API that handles everything related to write-only operations for user's tokenomics. +// @query.collection.format multi +// @schemes https +// @contact.name ice.io +// @contact.url https://ice.io +// @BasePath /v1w +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var cfg config + appCfg.MustLoadFromKey(applicationYamlKey, &cfg) + api.SwaggerInfo.Host = cfg.Host + api.SwaggerInfo.Version = cfg.Version + server.New(new(service), applicationYamlKey, swaggerRoot).ListenAndServe(ctx, cancel) +} + +func (s *service) RegisterRoutes(router *server.Router) { + s.setupTokenomicsRoutes(router) +} + +func (s *service) Init(ctx context.Context, cancel context.CancelFunc) { + s.tokenomicsProcessor = tokenomics.StartProcessor(ctx, cancel) +} + +func (s *service) Close(ctx context.Context) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "could not close processor because context ended") + } + + return errors.Wrap(s.tokenomicsProcessor.Close(), "could not close service") +} + +func (s *service) CheckHealth(ctx context.Context) error { + log.Debug("checking health...", "package", "tokenomics") + + return errors.Wrap(s.tokenomicsProcessor.CheckHealth(ctx), "failed to check processor's health") +} + +func contextWithHashCode[REQ, RESP any](ctx context.Context, req *server.Request[REQ, RESP]) context.Context { + switch hashCode := req.AuthenticatedUser.Claims["hashCode"].(type) { + case int: + return tokenomics.ContextWithHashCode(ctx, uint64(hashCode)) + case int64: + return tokenomics.ContextWithHashCode(ctx, uint64(hashCode)) + case uint64: + return tokenomics.ContextWithHashCode(ctx, hashCode) + case float64: + return tokenomics.ContextWithHashCode(ctx, uint64(hashCode)) + case string: + hc, err := strconv.ParseUint(hashCode, 10, 64) + log.Error(err) + + return tokenomics.ContextWithHashCode(ctx, hc) + default: + return ctx + } +} diff --git a/cmd/freezer-refrigerant/tokenomics.go b/cmd/freezer-refrigerant/tokenomics.go new file mode 100644 index 0000000..0ef8179 --- /dev/null +++ b/cmd/freezer-refrigerant/tokenomics.go @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: ice License 1.0 + +package main + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/freezer/tokenomics" + "github.com/ice-blockchain/wintr/server" + "github.com/ice-blockchain/wintr/terror" +) + +func (s *service) setupTokenomicsRoutes(router *server.Router) { + router. + Group("/v1w"). + POST("/tokenomics/:userId/mining-sessions", server.RootHandler(s.StartNewMiningSession)). + POST("/tokenomics/:userId/extra-bonus-claims", server.RootHandler(s.ClaimExtraBonus)). + PUT("/tokenomics/:userId/pre-staking", server.RootHandler(s.StartOrUpdatePreStaking)) +} + +// StartNewMiningSession godoc +// +// @Schemes +// @Description Starts a new mining session for the user, if not already in progress with another one. +// @Tags Tokenomics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param userId path string true "ID of the user" +// @Param request body StartNewMiningSessionRequestBody true "Request params" +// @Success 201 {object} tokenomics.MiningSummary +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if not allowed" +// @Failure 404 {object} server.ErrorResponse "if user not found" +// @Failure 409 {object} server.ErrorResponse "if mining is in progress or if a decision about negative mining progress is required" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics/{userId}/mining-sessions [POST]. +func (s *service) StartNewMiningSession( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[StartNewMiningSessionRequestBody, tokenomics.MiningSummary], +) (*server.Response[tokenomics.MiningSummary], *server.Response[server.ErrorResponse]) { + ms := &tokenomics.MiningSummary{MiningSession: &tokenomics.MiningSession{UserID: &req.Data.UserID}} + if err := s.tokenomicsProcessor.StartNewMiningSession(contextWithHashCode(ctx, req), ms, req.Data.Resurrect); err != nil { + err = errors.Wrapf(err, "failed to start a new mining session for userID:%v, data:%#v", req.Data.UserID, req.Data) + switch { + case errors.Is(err, tokenomics.ErrNegativeMiningProgressDecisionRequired): + if tErr := terror.As(err); tErr != nil { + return nil, server.Conflict(err, resurrectionDecisionRequiredErrorCode, tErr.Data) + } + + fallthrough + case errors.Is(err, tokenomics.ErrRaceCondition): + return nil, server.BadRequest(err, raceConditionErrorCode) + case errors.Is(err, tokenomics.ErrDuplicate): + return nil, server.Conflict(err, miningInProgressErrorCode) + case errors.Is(err, tokenomics.ErrRelationNotFound): + return nil, server.NotFound(err, userNotFoundErrorCode) + } + + return nil, server.Unexpected(err) + } + + return server.Created(ms), nil +} + +// ClaimExtraBonus godoc +// +// @Schemes +// @Description Claims an extra bonus for the user. +// @Tags Tokenomics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param userId path string true "ID of the user" +// @Success 201 {object} tokenomics.ExtraBonusSummary +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if not allowed" +// @Failure 404 {object} server.ErrorResponse "if user not found or no extra bonus available" +// @Failure 409 {object} server.ErrorResponse "if already claimed" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics/{userId}/extra-bonus-claims [POST]. +func (s *service) ClaimExtraBonus( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[ClaimExtraBonusRequestBody, tokenomics.ExtraBonusSummary], +) (*server.Response[tokenomics.ExtraBonusSummary], *server.Response[server.ErrorResponse]) { + resp := &tokenomics.ExtraBonusSummary{UserID: req.Data.UserID} + if err := s.tokenomicsProcessor.ClaimExtraBonus(contextWithHashCode(ctx, req), resp); err != nil { + err = errors.Wrapf(err, "failed to claim extra bonus for userID:%v", req.Data.UserID) + switch { + case errors.Is(err, tokenomics.ErrNotFound): + return nil, server.NotFound(err, noExtraBonusAvailableErrorCode) + case errors.Is(err, tokenomics.ErrDuplicate): + return nil, server.Conflict(err, extraBonusAlreadyClaimedErrorCode) + } + + return nil, server.Unexpected(err) + } + + return server.Created(resp), nil +} + +// StartOrUpdatePreStaking godoc +// +// @Schemes +// @Description Starts or updates pre-staking for the user. +// @Tags Tokenomics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param userId path string true "ID of the user" +// @Param request body StartOrUpdatePreStakingRequestBody true "Request params" +// @Success 200 {object} tokenomics.PreStakingSummary +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if not allowed" +// @Failure 404 {object} server.ErrorResponse "user not found" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics/{userId}/pre-staking [PUT]. +func (s *service) StartOrUpdatePreStaking( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[StartOrUpdatePreStakingRequestBody, tokenomics.PreStakingSummary], +) (*server.Response[tokenomics.PreStakingSummary], *server.Response[server.ErrorResponse]) { + const maxAllocation = 100 + if req.Data.Years > tokenomics.MaxPreStakingYears { + req.Data.Years = tokenomics.MaxPreStakingYears + } + if req.Data.Allocation > maxAllocation { + req.Data.Allocation = maxAllocation + } + st := &tokenomics.PreStakingSummary{ + PreStaking: &tokenomics.PreStaking{ + UserID: req.Data.UserID, + Years: uint64(req.Data.Years), + Allocation: uint64(req.Data.Allocation), + }, + } + if err := s.tokenomicsProcessor.StartOrUpdatePreStaking(contextWithHashCode(ctx, req), st); err != nil { + err = errors.Wrapf(err, "failed to StartOrUpdatePreStaking for %#v", req.Data) + switch { + case errors.Is(err, tokenomics.ErrDecreasingPreStakingAllocationOrYearsNotAllowed): + return nil, server.ForbiddenWithCode(err, decreasingPreStakingAllocationOrYearsNotAllowedErrorCode) + case errors.Is(err, tokenomics.ErrRelationNotFound): + return nil, server.NotFound(err, userNotFoundErrorCode) + } + + return nil, server.Unexpected(err) + } + + return server.OK(st), nil +} diff --git a/cmd/freezer/.testdata/application.yaml b/cmd/freezer/.testdata/application.yaml new file mode 100644 index 0000000..b27713e --- /dev/null +++ b/cmd/freezer/.testdata/application.yaml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: ice License 1.0 + +development: false +logger: + encoder: console + level: info +cmd/freezer: + host: localhost + version: latest + defaultEndpointTimeout: 5s + httpServer: + port: 44443 + certPath: .testdata/localhost.crt + keyPath: .testdata/localhost.key + defaultPagination: + limit: 20 + maxLimit: 1000 +#TODO \ No newline at end of file diff --git a/cmd/freezer/.testdata/expected_swagger.json b/cmd/freezer/.testdata/expected_swagger.json new file mode 100644 index 0000000..e69de29 diff --git a/cmd/freezer/.testdata/localhost.crt b/cmd/freezer/.testdata/localhost.crt new file mode 100644 index 0000000..84c6603 --- /dev/null +++ b/cmd/freezer/.testdata/localhost.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDijCCAnKgAwIBAgIJAMeawIdSd6+8MA0GCSqGSIb3DQEBCwUAMCcxCzAJBgNV +BAYTAlVTMRgwFgYDVQQDDA9FeGFtcGxlLVJvb3QtQ0EwHhcNMjIwMTA0MjEwNDE3 +WhcNMjQxMDI0MjEwNDE3WjBtMQswCQYDVQQGEwJVUzESMBAGA1UECAwJWW91clN0 +YXRlMREwDwYDVQQHDAhZb3VyQ2l0eTEdMBsGA1UECgwURXhhbXBsZS1DZXJ0aWZp +Y2F0ZXMxGDAWBgNVBAMMD2xvY2FsaG9zdC5sb2NhbDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAONuA1zntIXbNaEvt/n+/Jisib/8Bjvfm2I9ENMq0TBH +OGlbZgJ9ywiKsrxBYH/O2q6Dsxy9fL5cSfcMmAS0FXPrcXQx/pVNCgNWLEXZyPDk +SzSR+tlPXzuryN2/jbWtgOZc73kfxQVBqUWbLyMiXaxMxVGHgpYMg0w68Ee62d2H +AnA7c0YBllvggDRSaoDRJJZTc8DDGAHm9x5583zdxpCQh/EeV+zIjd2lAGF0ioYu +PV69lwyrTnY/s7WG59nRYwYR50JvbI4G+5bbpf4q2W7Q0BVLqwSdMJfAfG43N5U/ +4Q1dfyJeXavFfQaZWJtEiVOU9TBiV3QQto0tI28R6J0CAwEAAaNzMHEwQQYDVR0j +BDowOKErpCkwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1D +QYIJANxKhfP/dJTMMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuC +CWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAjrUp0epptzaaTULvhrFdNJ6e +2WAeJpYCxMXjms7P+B/ldyIirDqG/WEzpN64Z1gXJhtxnw7IGTsQ8eXqLmBDk045 +vHhVbRhjVGADc+EVwX6OzQ+WQEGZzNDPX7DBObLC1ZV5LcfUwQXyACmlARlYgXJN +GZFDkijDcvY3/Hyq9NkV6VGYPKnzxaal3v3cYO8FXQHaOLnu+SLWknT56y2vTa5/ +H4CoX8nrts5Fa0NuOdoyNA1c7IdHjR/dy4g5IUZW+Sbhr1nNgkECBJvJ5QOWZ3M4 +4a8NroD0ikzQDaeS4Tpk54WnJLEjDgQe5fX9RMu9F2sbr+wP+gUTmHuhLg/Ptw== +-----END CERTIFICATE----- diff --git a/cmd/freezer/.testdata/localhost.key b/cmd/freezer/.testdata/localhost.key new file mode 100644 index 0000000..f2fa28b --- /dev/null +++ b/cmd/freezer/.testdata/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDjbgNc57SF2zWh +L7f5/vyYrIm//AY735tiPRDTKtEwRzhpW2YCfcsIirK8QWB/ztqug7McvXy+XEn3 +DJgEtBVz63F0Mf6VTQoDVixF2cjw5Es0kfrZT187q8jdv421rYDmXO95H8UFQalF +my8jIl2sTMVRh4KWDINMOvBHutndhwJwO3NGAZZb4IA0UmqA0SSWU3PAwxgB5vce +efN83caQkIfxHlfsyI3dpQBhdIqGLj1evZcMq052P7O1hufZ0WMGEedCb2yOBvuW +26X+Ktlu0NAVS6sEnTCXwHxuNzeVP+ENXX8iXl2rxX0GmVibRIlTlPUwYld0ELaN +LSNvEeidAgMBAAECggEBALHtN6RPgePXA7X+5ygmXOf01C/ms9nTrnTE4YzTSqVC +kteaMcxxLY6ZNAwj+aMD6gHt9wrdE+K5wQQOTkAfw0jVQgVtt4aGpvbFTA25vIL5 +l/yg2Gd6uT6tvo/9dJhWDSosOw2/1RuvqwZRyibqk+5ggV6vbXKGh5Hz6lezzw6H +P8xazcT634Tj5YhNhd00XIcr1V+kqEHZGiJP0XzrdXzjAS5NciEdW529gv4Dp4Ni +zpSroznCcP6psLXS99snDg1UdQPFu90IW51i7VOBkF+RhRIMWOywO9FeFHoQ7j0u +SqACHFz8HQnR0uSZ8AwnWrRhWVoBfQ6bwDjJKi/vtQECgYEA8ZxQtliNEd2ojF0s +PbU7YE9vTDEY5AXk6bRPf1rJk/RTDZZwguC4MWjTBpcqawppzur8RLRJAp3WtyP4 +zXh7qvgeOFIaVmGUefEfg8OkXAtvwT+ogvl9HHyY3lPWQyF+WV3dN4ILWguDYiCB +myL/4EqBZjSHmqfzKS6sT9x+TYkCgYEA8Pl9uH6wDSReKqmO1kNnyF+dWfP0I7wX +UpSTkRvSrYQIH2VFYH+LSN5OZaku0FHQkIbgjunAT29N8p//E2ZA3L2xNIKDV+hI +M+NV52YwguUROh2mIypGlPT1f7R+tiYzz27jZgctYIF3mzTMQ1TC2TqgXzG5eA2y +/Ojcmj9ncXUCgYEA4y5fOkYjR3RMAsetTMy3awTmGxdjVy0vpIx138NHHYaz/WfC +nV2d9F+jZWQIb6PX/8c2s4dtyzcM6SG61cD/T7CEAeM5fpW8XbjbMDNqvV3HlEc+ +NQFQodOKjir4oiDBRFidJI90CxQeUstL8srDHGwSJj8obsSTQNrxDRq/7DkCgYBR +cLBpmv9a4bClkHqCtXMsyAvA6+7V6Oqk8SvSPen81IN+QNaqn1BuhxtNxljY9N2d +Csh35E4nSoG4fxRQ9Rz0vXNXQMis/Aby6mEM/H9mrY4d6wlMFyyViRgzWcf9PXoD +IAHgaIqQdBD9NmHWW54ilmq+4WpCRbb5PKXZx5XpRQKBgQCCMpaANqren/4aeDdz +F2lkEJweRsTaS13LJKkk/fGWeXo3N/sXuBPocViSzkCNoHGx1yHrG9TyC7Cz7UXj +4Dpy7gI3cg0i7gaHgC1JfYoPzCSmvnJT62TyL/5SGwF4Xkg8efmF+sVKZqsqgiiT +ATGyCMbfg4XaTw84ubV2rGxvRQ== +-----END PRIVATE KEY----- diff --git a/cmd/freezer/Dockerfile b/cmd/freezer/Dockerfile new file mode 100644 index 0000000..f913afd --- /dev/null +++ b/cmd/freezer/Dockerfile @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: ice License 1.0 + +FROM golang:latest AS build +ARG SERVICE_NAME +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /app/ +COPY . /app/ + +ENV CGO_ENABLED=1 +ENV GOOS=$TARGETOS +ENV GOARCH=$TARGETARCH + +RUN env SERVICE_NAME=$SERVICE_NAME make dockerfile +RUN cp cmd/$SERVICE_NAME/bin bin + +FROM gcr.io/distroless/base-debian11:latest +ARG TARGETOS +ARG TARGETARCH +ARG PORT=443 +LABEL os=$TARGETOS +LABEL arch=$TARGETARCH +COPY --from=build /app/bin app +#You might need to expose more ports. Just add more separated by space +#I.E. EXPOSE 8080 8081 8082 8083 +EXPOSE $PORT +ENTRYPOINT ["/app"] diff --git a/cmd/freezer/api/docs.go b/cmd/freezer/api/docs.go new file mode 100644 index 0000000..b45134c --- /dev/null +++ b/cmd/freezer/api/docs.go @@ -0,0 +1,895 @@ +// Code generated by swaggo/swag. DO NOT EDIT. + +package api + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "ice.io", + "url": "https://ice.io" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/tokenomics-statistics/adoption": { + "get": { + "description": "Returns the current adoption information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statistics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.AdoptionSummary" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics-statistics/top-miners": { + "get": { + "description": "Returns the paginated leaderboard with top miners.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statistics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "a keyword to look for in the user's username or firstname/lastname", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "max number of elements to return. Default is ` + "`" + `10` + "`" + `.", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "number of elements to skip before starting to fetch data", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/tokenomics.Miner" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/balance-history": { + "get": { + "description": "Returns the balance history for the provided params.\nIf ` + "`" + `startDate` + "`" + ` is after ` + "`" + `endDate` + "`" + `, we go backwards in time: I.E. today, yesterday, etc.\nIf ` + "`" + `startDate` + "`" + ` is before ` + "`" + `endDate` + "`" + `, we go forwards in time: I.E. today, tomorrow, etc.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The start date in RFC3339 or ISO8601 formats. Default is ` + "`" + `now` + "`" + ` in UTC.", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "description": "The start date in RFC3339 or ISO8601 formats. Default is ` + "`" + `end of day, relative to startDate` + "`" + `.", + "name": "endDate", + "in": "query" + }, + { + "type": "string", + "description": "The user's timezone. I.E. ` + "`" + `+03:00` + "`" + `, ` + "`" + `-1:30` + "`" + `. Default is UTC.", + "name": "tz", + "in": "query" + }, + { + "type": "integer", + "description": "max number of elements to return. Default is ` + "`" + `24` + "`" + `.", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "number of elements to skip before starting to fetch data", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/tokenomics.BalanceHistoryEntry" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/balance-summary": { + "get": { + "description": "Returns the balance related information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.BalanceSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/mining-summary": { + "get": { + "description": "Returns the mining related information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.MiningSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/pre-staking-summary": { + "get": { + "description": "Returns the pre-staking related information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.PreStakingSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/ranking-summary": { + "get": { + "description": "Returns the ranking related information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.RankingSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if hidden by the user", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "server.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "SOMETHING_NOT_FOUND" + }, + "data": { + "type": "object", + "additionalProperties": {} + }, + "error": { + "type": "string", + "example": "something is missing" + } + } + }, + "tokenomics.AdoptionSummary": { + "type": "object", + "properties": { + "milestones": { + "type": "array", + "items": { + "type": "object", + "properties": { + "achievedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "baseMiningRate": { + "type": "string", + "example": "1,243.02" + }, + "milestone": { + "type": "integer", + "example": 1 + }, + "totalActiveUsers": { + "type": "integer", + "example": 1 + } + } + } + }, + "totalActiveUsers": { + "type": "integer", + "example": 11 + } + } + }, + "tokenomics.BalanceHistoryBalanceDiff": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "example": "1,243.02" + }, + "bonus": { + "type": "integer", + "example": 120 + }, + "negative": { + "type": "boolean", + "example": true + } + } + }, + "tokenomics.BalanceHistoryEntry": { + "type": "object", + "properties": { + "balance": { + "$ref": "#/definitions/tokenomics.BalanceHistoryBalanceDiff" + }, + "time": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "timeSeries": { + "type": "array", + "items": { + "$ref": "#/definitions/tokenomics.BalanceHistoryEntry" + } + } + } + }, + "tokenomics.BalanceSummary": { + "type": "object", + "properties": { + "preStaking": { + "type": "string", + "example": "1,243.02" + }, + "standard": { + "type": "string", + "example": "1,243.02" + }, + "t1": { + "type": "string", + "example": "1,243.02" + }, + "t2": { + "type": "string", + "example": "1,243.02" + }, + "total": { + "type": "string", + "example": "1,243.02" + }, + "totalNoPreStakingBonus": { + "type": "string", + "example": "1,243.02" + }, + "totalReferrals": { + "type": "string", + "example": "1,243.02" + } + } + }, + "tokenomics.Miner": { + "type": "object", + "properties": { + "balance": { + "type": "string", + "example": "12345.6334" + }, + "profilePictureUrl": { + "type": "string", + "example": "https://somecdn.com/p1.jpg" + }, + "userId": { + "type": "string", + "example": "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "username": { + "type": "string", + "example": "jdoe" + } + } + }, + "tokenomics.MiningRateBonuses": { + "type": "object", + "properties": { + "extra": { + "type": "integer", + "example": 300 + }, + "preStaking": { + "type": "integer", + "example": 300 + }, + "t1": { + "type": "integer", + "example": 100 + }, + "t2": { + "type": "integer", + "example": 200 + }, + "total": { + "type": "integer", + "example": 300 + } + } + }, + "tokenomics.MiningRateSummary-coin_ICE": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "example": "1,234,232.001" + }, + "bonuses": { + "$ref": "#/definitions/tokenomics.MiningRateBonuses" + } + } + }, + "tokenomics.MiningRateType": { + "type": "string", + "enum": [ + "positive", + "negative", + "none" + ], + "x-enum-varnames": [ + "PositiveMiningRateType", + "NegativeMiningRateType", + "NoneMiningRateType" + ] + }, + "tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "positiveTotalNoPreStakingBonus": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "preStaking": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "standard": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "total": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "totalNoPreStakingBonus": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "type": { + "$ref": "#/definitions/tokenomics.MiningRateType" + } + } + }, + "tokenomics.MiningSession": { + "type": "object", + "properties": { + "endedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "free": { + "type": "boolean", + "example": true + }, + "resettableStartingAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "startedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "warnAboutExpirationStartingAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + } + } + }, + "tokenomics.MiningSummary": { + "type": "object", + "properties": { + "availableExtraBonus": { + "type": "integer", + "example": 2 + }, + "miningRates": { + "$ref": "#/definitions/tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE" + }, + "miningSession": { + "$ref": "#/definitions/tokenomics.MiningSession" + }, + "miningStreak": { + "type": "integer", + "example": 2 + }, + "remainingFreeMiningSessions": { + "type": "integer", + "example": 1 + } + } + }, + "tokenomics.PreStakingSummary": { + "type": "object", + "properties": { + "allocation": { + "type": "integer", + "example": 100 + }, + "bonus": { + "type": "integer", + "example": 100 + }, + "years": { + "type": "integer", + "example": 1 + } + } + }, + "tokenomics.RankingSummary": { + "type": "object", + "properties": { + "globalRank": { + "type": "integer", + "example": 12333 + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "latest", + Host: "", + BasePath: "/v1r", + Schemes: []string{"https"}, + Title: "Tokenomics API", + Description: "API that handles everything related to read-only operations for user's tokenomics and statistics about it.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/cmd/freezer/api/swagger.json b/cmd/freezer/api/swagger.json new file mode 100644 index 0000000..662fc82 --- /dev/null +++ b/cmd/freezer/api/swagger.json @@ -0,0 +1,874 @@ +{ + "schemes": [ + "https" + ], + "swagger": "2.0", + "info": { + "description": "API that handles everything related to read-only operations for user's tokenomics and statistics about it.", + "title": "Tokenomics API", + "contact": { + "name": "ice.io", + "url": "https://ice.io" + }, + "version": "latest" + }, + "basePath": "/v1r", + "paths": { + "/tokenomics-statistics/adoption": { + "get": { + "description": "Returns the current adoption information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statistics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.AdoptionSummary" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics-statistics/top-miners": { + "get": { + "description": "Returns the paginated leaderboard with top miners.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statistics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "a keyword to look for in the user's username or firstname/lastname", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "max number of elements to return. Default is `10`.", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "number of elements to skip before starting to fetch data", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/tokenomics.Miner" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/balance-history": { + "get": { + "description": "Returns the balance history for the provided params.\nIf `startDate` is after `endDate`, we go backwards in time: I.E. today, yesterday, etc.\nIf `startDate` is before `endDate`, we go forwards in time: I.E. today, tomorrow, etc.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The start date in RFC3339 or ISO8601 formats. Default is `now` in UTC.", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "description": "The start date in RFC3339 or ISO8601 formats. Default is `end of day, relative to startDate`.", + "name": "endDate", + "in": "query" + }, + { + "type": "string", + "description": "The user's timezone. I.E. `+03:00`, `-1:30`. Default is UTC.", + "name": "tz", + "in": "query" + }, + { + "type": "integer", + "description": "max number of elements to return. Default is `24`.", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "number of elements to skip before starting to fetch data", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/tokenomics.BalanceHistoryEntry" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/balance-summary": { + "get": { + "description": "Returns the balance related information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.BalanceSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/mining-summary": { + "get": { + "description": "Returns the mining related information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.MiningSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/pre-staking-summary": { + "get": { + "description": "Returns the pre-staking related information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.PreStakingSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/tokenomics/{userId}/ranking-summary": { + "get": { + "description": "Returns the ranking related information.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tokenomics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tokenomics.RankingSummary" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if hidden by the user", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "server.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "SOMETHING_NOT_FOUND" + }, + "data": { + "type": "object", + "additionalProperties": {} + }, + "error": { + "type": "string", + "example": "something is missing" + } + } + }, + "tokenomics.AdoptionSummary": { + "type": "object", + "properties": { + "milestones": { + "type": "array", + "items": { + "type": "object", + "properties": { + "achievedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "baseMiningRate": { + "type": "string", + "example": "1,243.02" + }, + "milestone": { + "type": "integer", + "example": 1 + }, + "totalActiveUsers": { + "type": "integer", + "example": 1 + } + } + } + }, + "totalActiveUsers": { + "type": "integer", + "example": 11 + } + } + }, + "tokenomics.BalanceHistoryBalanceDiff": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "example": "1,243.02" + }, + "bonus": { + "type": "integer", + "example": 120 + }, + "negative": { + "type": "boolean", + "example": true + } + } + }, + "tokenomics.BalanceHistoryEntry": { + "type": "object", + "properties": { + "balance": { + "$ref": "#/definitions/tokenomics.BalanceHistoryBalanceDiff" + }, + "time": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "timeSeries": { + "type": "array", + "items": { + "$ref": "#/definitions/tokenomics.BalanceHistoryEntry" + } + } + } + }, + "tokenomics.BalanceSummary": { + "type": "object", + "properties": { + "preStaking": { + "type": "string", + "example": "1,243.02" + }, + "standard": { + "type": "string", + "example": "1,243.02" + }, + "t1": { + "type": "string", + "example": "1,243.02" + }, + "t2": { + "type": "string", + "example": "1,243.02" + }, + "total": { + "type": "string", + "example": "1,243.02" + }, + "totalNoPreStakingBonus": { + "type": "string", + "example": "1,243.02" + }, + "totalReferrals": { + "type": "string", + "example": "1,243.02" + } + } + }, + "tokenomics.Miner": { + "type": "object", + "properties": { + "balance": { + "type": "string", + "example": "12345.6334" + }, + "profilePictureUrl": { + "type": "string", + "example": "https://somecdn.com/p1.jpg" + }, + "userId": { + "type": "string", + "example": "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "username": { + "type": "string", + "example": "jdoe" + } + } + }, + "tokenomics.MiningRateBonuses": { + "type": "object", + "properties": { + "extra": { + "type": "integer", + "example": 300 + }, + "preStaking": { + "type": "integer", + "example": 300 + }, + "t1": { + "type": "integer", + "example": 100 + }, + "t2": { + "type": "integer", + "example": 200 + }, + "total": { + "type": "integer", + "example": 300 + } + } + }, + "tokenomics.MiningRateSummary-coin_ICE": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "example": "1,234,232.001" + }, + "bonuses": { + "$ref": "#/definitions/tokenomics.MiningRateBonuses" + } + } + }, + "tokenomics.MiningRateType": { + "type": "string", + "enum": [ + "positive", + "negative", + "none" + ], + "x-enum-varnames": [ + "PositiveMiningRateType", + "NegativeMiningRateType", + "NoneMiningRateType" + ] + }, + "tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE": { + "type": "object", + "properties": { + "base": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "positiveTotalNoPreStakingBonus": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "preStaking": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "standard": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "total": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "totalNoPreStakingBonus": { + "$ref": "#/definitions/tokenomics.MiningRateSummary-coin_ICE" + }, + "type": { + "$ref": "#/definitions/tokenomics.MiningRateType" + } + } + }, + "tokenomics.MiningSession": { + "type": "object", + "properties": { + "endedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "free": { + "type": "boolean", + "example": true + }, + "resettableStartingAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "startedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "warnAboutExpirationStartingAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + } + } + }, + "tokenomics.MiningSummary": { + "type": "object", + "properties": { + "availableExtraBonus": { + "type": "integer", + "example": 2 + }, + "miningRates": { + "$ref": "#/definitions/tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE" + }, + "miningSession": { + "$ref": "#/definitions/tokenomics.MiningSession" + }, + "miningStreak": { + "type": "integer", + "example": 2 + }, + "remainingFreeMiningSessions": { + "type": "integer", + "example": 1 + } + } + }, + "tokenomics.PreStakingSummary": { + "type": "object", + "properties": { + "allocation": { + "type": "integer", + "example": 100 + }, + "bonus": { + "type": "integer", + "example": 100 + }, + "years": { + "type": "integer", + "example": 1 + } + } + }, + "tokenomics.RankingSummary": { + "type": "object", + "properties": { + "globalRank": { + "type": "integer", + "example": 12333 + } + } + } + } +} \ No newline at end of file diff --git a/cmd/freezer/api/swagger.yaml b/cmd/freezer/api/swagger.yaml new file mode 100644 index 0000000..ae2a769 --- /dev/null +++ b/cmd/freezer/api/swagger.yaml @@ -0,0 +1,600 @@ +# SPDX-License-Identifier: ice License 1.0 + +basePath: /v1r +definitions: + server.ErrorResponse: + properties: + code: + example: SOMETHING_NOT_FOUND + type: string + data: + additionalProperties: {} + type: object + error: + example: something is missing + type: string + type: object + tokenomics.AdoptionSummary: + properties: + milestones: + items: + properties: + achievedAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + baseMiningRate: + example: 1,243.02 + type: string + milestone: + example: 1 + type: integer + totalActiveUsers: + example: 1 + type: integer + type: object + type: array + totalActiveUsers: + example: 11 + type: integer + type: object + tokenomics.BalanceHistoryBalanceDiff: + properties: + amount: + example: 1,243.02 + type: string + bonus: + example: 120 + type: integer + negative: + example: true + type: boolean + type: object + tokenomics.BalanceHistoryEntry: + properties: + balance: + $ref: '#/definitions/tokenomics.BalanceHistoryBalanceDiff' + time: + example: "2022-01-03T16:20:52.156534Z" + type: string + timeSeries: + items: + $ref: '#/definitions/tokenomics.BalanceHistoryEntry' + type: array + type: object + tokenomics.BalanceSummary: + properties: + preStaking: + example: 1,243.02 + type: string + standard: + example: 1,243.02 + type: string + t1: + example: 1,243.02 + type: string + t2: + example: 1,243.02 + type: string + total: + example: 1,243.02 + type: string + totalNoPreStakingBonus: + example: 1,243.02 + type: string + totalReferrals: + example: 1,243.02 + type: string + type: object + tokenomics.Miner: + properties: + balance: + example: "12345.6334" + type: string + profilePictureUrl: + example: https://somecdn.com/p1.jpg + type: string + userId: + example: did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2 + type: string + username: + example: jdoe + type: string + type: object + tokenomics.MiningRateBonuses: + properties: + extra: + example: 300 + type: integer + preStaking: + example: 300 + type: integer + t1: + example: 100 + type: integer + t2: + example: 200 + type: integer + total: + example: 300 + type: integer + type: object + tokenomics.MiningRateSummary-coin_ICE: + properties: + amount: + example: 1,234,232.001 + type: string + bonuses: + $ref: '#/definitions/tokenomics.MiningRateBonuses' + type: object + tokenomics.MiningRateType: + enum: + - positive + - negative + - none + type: string + x-enum-varnames: + - PositiveMiningRateType + - NegativeMiningRateType + - NoneMiningRateType + tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE: + properties: + base: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + positiveTotalNoPreStakingBonus: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + preStaking: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + standard: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + total: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + totalNoPreStakingBonus: + $ref: '#/definitions/tokenomics.MiningRateSummary-coin_ICE' + type: + $ref: '#/definitions/tokenomics.MiningRateType' + type: object + tokenomics.MiningSession: + properties: + endedAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + free: + example: true + type: boolean + resettableStartingAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + startedAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + warnAboutExpirationStartingAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + type: object + tokenomics.MiningSummary: + properties: + availableExtraBonus: + example: 2 + type: integer + miningRates: + $ref: '#/definitions/tokenomics.MiningRates-tokenomics_MiningRateSummary-coin_ICE' + miningSession: + $ref: '#/definitions/tokenomics.MiningSession' + miningStreak: + example: 2 + type: integer + remainingFreeMiningSessions: + example: 1 + type: integer + type: object + tokenomics.PreStakingSummary: + properties: + allocation: + example: 100 + type: integer + bonus: + example: 100 + type: integer + years: + example: 1 + type: integer + type: object + tokenomics.RankingSummary: + properties: + globalRank: + example: 12333 + type: integer + type: object +info: + contact: + name: ice.io + url: https://ice.io + description: API that handles everything related to read-only operations for user's + tokenomics and statistics about it. + title: Tokenomics API + version: latest +paths: + /tokenomics-statistics/adoption: + get: + consumes: + - application/json + description: Returns the current adoption information. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tokenomics.AdoptionSummary' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Statistics + /tokenomics-statistics/top-miners: + get: + consumes: + - application/json + description: Returns the paginated leaderboard with top miners. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: a keyword to look for in the user's username or firstname/lastname + in: query + name: keyword + type: string + - description: max number of elements to return. Default is `10`. + in: query + name: limit + type: integer + - description: number of elements to skip before starting to fetch data + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/tokenomics.Miner' + type: array + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Statistics + /tokenomics/{userId}/balance-history: + get: + consumes: + - application/json + description: |- + Returns the balance history for the provided params. + If `startDate` is after `endDate`, we go backwards in time: I.E. today, yesterday, etc. + If `startDate` is before `endDate`, we go forwards in time: I.E. today, tomorrow, etc. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + - description: The start date in RFC3339 or ISO8601 formats. Default is `now` + in UTC. + in: query + name: startDate + type: string + - description: The start date in RFC3339 or ISO8601 formats. Default is `end + of day, relative to startDate`. + in: query + name: endDate + type: string + - description: The user's timezone. I.E. `+03:00`, `-1:30`. Default is UTC. + in: query + name: tz + type: string + - description: max number of elements to return. Default is `24`. + in: query + name: limit + type: integer + - description: number of elements to skip before starting to fetch data + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/tokenomics.BalanceHistoryEntry' + type: array + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if not allowed + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Tokenomics + /tokenomics/{userId}/balance-summary: + get: + consumes: + - application/json + description: Returns the balance related information. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tokenomics.BalanceSummary' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if not allowed + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Tokenomics + /tokenomics/{userId}/mining-summary: + get: + consumes: + - application/json + description: Returns the mining related information. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tokenomics.MiningSummary' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if not allowed + schema: + $ref: '#/definitions/server.ErrorResponse' + "404": + description: if not found + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Tokenomics + /tokenomics/{userId}/pre-staking-summary: + get: + consumes: + - application/json + description: Returns the pre-staking related information. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tokenomics.PreStakingSummary' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if not allowed + schema: + $ref: '#/definitions/server.ErrorResponse' + "404": + description: if not found + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Tokenomics + /tokenomics/{userId}/ranking-summary: + get: + consumes: + - application/json + description: Returns the ranking related information. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tokenomics.RankingSummary' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if hidden by the user + schema: + $ref: '#/definitions/server.ErrorResponse' + "404": + description: if not found + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Tokenomics +schemes: +- https +swagger: "2.0" diff --git a/cmd/freezer/contract.go b/cmd/freezer/contract.go new file mode 100644 index 0000000..8a33bf6 --- /dev/null +++ b/cmd/freezer/contract.go @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: ice License 1.0 + +package main + +import ( + stdlibtime "time" + + "github.com/ice-blockchain/freezer/tokenomics" +) + +// Public API. + +type ( + GetMiningSummaryArg struct { + UserID string `uri:"userId" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + } + GetPreStakingSummaryArg struct { + UserID string `uri:"userId" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + } + GetBalanceSummaryArg struct { + UserID string `uri:"userId" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + } + GetBalanceHistoryArg struct { + // The start date in RFC3339 or ISO8601 formats. Default is `now` in UTC. + StartDate *stdlibtime.Time `form:"startDate" swaggertype:"string" example:"2022-01-03T16:20:52.156534Z"` + // The start date in RFC3339 or ISO8601 formats. Default is `end of day, relative to startDate`. + EndDate *stdlibtime.Time `form:"endDate" swaggertype:"string" example:"2022-01-03T16:20:52.156534Z"` + UserID string `uri:"userId" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + TZ string `form:"tz" example:"-03:00"` + // Default is 24. + Limit uint64 `form:"limit" maximum:"1000" example:"24"` + Offset uint64 `form:"offset" example:"0"` + } + GetRankingSummaryArg struct { + UserID string `uri:"userId" allowForbiddenGet:"true" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + } + GetTopMinersArg struct { + Keyword string `form:"keyword" example:"jdoe"` + // Default is 10. + Limit uint64 `form:"limit" maximum:"1000" example:"10"` + Offset uint64 `form:"offset" example:"0"` + } + GetAdoptionArg struct{} +) + +// Private API. + +const ( + applicationYamlKey = "cmd/freezer" + swaggerRoot = "/tokenomics/r" +) + +// Values for server.ErrorResponse#Code. +const ( + userNotFoundErrorCode = "USER_NOT_FOUND" + userPreStakingNotEnabledErrorCode = "PRE_STAKING_NOT_ENABLED" + globalRankHiddenErrorCode = "GLOBAL_RANK_HIDDEN" + invalidPropertiesErrorCode = "INVALID_PROPERTIES" +) + +type ( + // | service implements server.State and is responsible for managing the state and lifecycle of the package. + service struct { + tokenomicsRepository tokenomics.Repository + } + config struct { + Host string `yaml:"host"` + Version string `yaml:"version"` + } +) diff --git a/cmd/freezer/freezer.go b/cmd/freezer/freezer.go new file mode 100644 index 0000000..023db13 --- /dev/null +++ b/cmd/freezer/freezer.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: ice License 1.0 + +package main + +import ( + "context" + "strconv" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/freezer/cmd/freezer/api" + "github.com/ice-blockchain/freezer/tokenomics" + appCfg "github.com/ice-blockchain/wintr/config" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/server" +) + +// @title Tokenomics API +// @version latest +// @description API that handles everything related to read-only operations for user's tokenomics and statistics about it. +// @query.collection.format multi +// @schemes https +// @contact.name ice.io +// @contact.url https://ice.io +// @BasePath /v1r +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var cfg config + appCfg.MustLoadFromKey(applicationYamlKey, &cfg) + api.SwaggerInfo.Host = cfg.Host + api.SwaggerInfo.Version = cfg.Version + server.New(new(service), applicationYamlKey, swaggerRoot).ListenAndServe(ctx, cancel) +} + +func (s *service) RegisterRoutes(router *server.Router) { + s.setupTokenomicsRoutes(router) + s.setupStatisticsRoutes(router) +} + +func (s *service) Init(ctx context.Context, cancel context.CancelFunc) { + s.tokenomicsRepository = tokenomics.New(ctx, cancel) +} + +func (s *service) Close(ctx context.Context) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "could not close repository because context ended") + } + + return errors.Wrap(s.tokenomicsRepository.Close(), "could not close repository") +} + +func (s *service) CheckHealth(ctx context.Context) error { + log.Debug("checking health...", "package", "tokenomics") + _, err := s.tokenomicsRepository.GetAdoptionSummary(ctx) + + return errors.Wrap(err, "get AdoptionSummary failed") +} + +func contextWithHashCode[REQ, RESP any](ctx context.Context, req *server.Request[REQ, RESP]) context.Context { + switch hashCode := req.AuthenticatedUser.Claims["hashCode"].(type) { + case int: + return tokenomics.ContextWithHashCode(ctx, uint64(hashCode)) + case int64: + return tokenomics.ContextWithHashCode(ctx, uint64(hashCode)) + case uint64: + return tokenomics.ContextWithHashCode(ctx, hashCode) + case float64: + return tokenomics.ContextWithHashCode(ctx, uint64(hashCode)) + case string: + hc, err := strconv.ParseUint(hashCode, 10, 64) + log.Error(err) + + return tokenomics.ContextWithHashCode(ctx, hc) + default: + return ctx + } +} diff --git a/cmd/freezer/statistics.go b/cmd/freezer/statistics.go new file mode 100644 index 0000000..b59aa08 --- /dev/null +++ b/cmd/freezer/statistics.go @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: ice License 1.0 + +package main + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/freezer/tokenomics" + "github.com/ice-blockchain/wintr/server" +) + +func (s *service) setupStatisticsRoutes(router *server.Router) { + router. + Group("/v1r"). + GET("/tokenomics-statistics/top-miners", server.RootHandler(s.GetTopMiners)). + GET("/tokenomics-statistics/adoption", server.RootHandler(s.GetAdoption)) +} + +// GetTopMiners godoc +// +// @Schemes +// @Description Returns the paginated leaderboard with top miners. +// @Tags Statistics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param keyword query string false "a keyword to look for in the user's username or firstname/lastname" +// @Param limit query uint64 false "max number of elements to return. Default is `10`." +// @Param offset query uint64 false "number of elements to skip before starting to fetch data" +// @Success 200 {array} tokenomics.Miner +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics-statistics/top-miners [GET]. +func (s *service) GetTopMiners( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetTopMinersArg, []*tokenomics.Miner], +) (*server.Response[[]*tokenomics.Miner], *server.Response[server.ErrorResponse]) { + const defaultLimit, maxLimit = 10, 1000 + if req.Data.Limit == 0 { + req.Data.Limit = defaultLimit + } + if req.Data.Limit > maxLimit { + req.Data.Limit = maxLimit + } + resp, err := s.tokenomicsRepository.GetTopMiners(ctx, req.Data.Keyword, req.Data.Limit, req.Data.Offset) + if err != nil { + return nil, server.Unexpected(errors.Wrapf(err, "failed to get top miners for userID:%v & req:%#v", req.AuthenticatedUser.UserID, req.Data)) + } + + return server.OK(&resp), nil +} + +// GetAdoption godoc +// +// @Schemes +// @Description Returns the current adoption information. +// @Tags Statistics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Success 200 {object} tokenomics.AdoptionSummary +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics-statistics/adoption [GET]. +func (s *service) GetAdoption( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetAdoptionArg, tokenomics.AdoptionSummary], +) (*server.Response[tokenomics.AdoptionSummary], *server.Response[server.ErrorResponse]) { + resp, err := s.tokenomicsRepository.GetAdoptionSummary(ctx) + if err != nil { + return nil, server.Unexpected(errors.Wrapf(err, "failed to get adoption summary for userID:%v", req.AuthenticatedUser.UserID)) + } + + return server.OK(resp), nil +} diff --git a/cmd/freezer/tokenomics.go b/cmd/freezer/tokenomics.go new file mode 100644 index 0000000..8d5e826 --- /dev/null +++ b/cmd/freezer/tokenomics.go @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: ice License 1.0 + +package main + +import ( + "context" + "strings" + stdlibtime "time" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/freezer/tokenomics" + "github.com/ice-blockchain/wintr/server" + "github.com/ice-blockchain/wintr/time" +) + +func (s *service) setupTokenomicsRoutes(router *server.Router) { + router. + Group("/v1r"). + GET("/tokenomics/:userId/mining-summary", server.RootHandler(s.GetMiningSummary)). + GET("/tokenomics/:userId/pre-staking-summary", server.RootHandler(s.GetPreStakingSummary)). + GET("/tokenomics/:userId/balance-summary", server.RootHandler(s.GetBalanceSummary)). + GET("/tokenomics/:userId/balance-history", server.RootHandler(s.GetBalanceHistory)). + GET("/tokenomics/:userId/ranking-summary", server.RootHandler(s.GetRankingSummary)) +} + +// GetMiningSummary godoc +// +// @Schemes +// @Description Returns the mining related information. +// @Tags Tokenomics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param userId path string true "ID of the user" +// @Success 200 {object} tokenomics.MiningSummary +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if not allowed" +// @Failure 404 {object} server.ErrorResponse "if not found" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics/{userId}/mining-summary [GET]. +func (s *service) GetMiningSummary( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetMiningSummaryArg, tokenomics.MiningSummary], +) (*server.Response[tokenomics.MiningSummary], *server.Response[server.ErrorResponse]) { + mining, err := s.tokenomicsRepository.GetMiningSummary(contextWithHashCode(ctx, req), req.Data.UserID) + if err != nil { + err = errors.Wrapf(err, "failed to get user's mining summary for userID:%v", req.Data.UserID) + if errors.Is(err, tokenomics.ErrRelationNotFound) { + return nil, server.NotFound(err, userNotFoundErrorCode) + } + + return nil, server.Unexpected(err) + } + + return server.OK(mining), nil +} + +// GetPreStakingSummary godoc +// +// @Schemes +// @Description Returns the pre-staking related information. +// @Tags Tokenomics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param userId path string true "ID of the user" +// @Success 200 {object} tokenomics.PreStakingSummary +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if not allowed" +// @Failure 404 {object} server.ErrorResponse "if not found" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics/{userId}/pre-staking-summary [GET]. +func (s *service) GetPreStakingSummary( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetPreStakingSummaryArg, tokenomics.PreStakingSummary], +) (*server.Response[tokenomics.PreStakingSummary], *server.Response[server.ErrorResponse]) { + preStaking, err := s.tokenomicsRepository.GetPreStakingSummary(contextWithHashCode(ctx, req), req.Data.UserID) + if err != nil { + err = errors.Wrapf(err, "failed to get user's pre-staking summary for userID:%v", req.Data.UserID) + if errors.Is(err, tokenomics.ErrNotFound) { + return nil, server.NotFound(err, userPreStakingNotEnabledErrorCode) + } + + return nil, server.Unexpected(err) + } + + return server.OK(preStaking), nil +} + +// GetBalanceSummary godoc +// +// @Schemes +// @Description Returns the balance related information. +// @Tags Tokenomics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param userId path string true "ID of the user" +// @Success 200 {object} tokenomics.BalanceSummary +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if not allowed" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics/{userId}/balance-summary [GET]. +func (s *service) GetBalanceSummary( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetBalanceSummaryArg, tokenomics.BalanceSummary], +) (*server.Response[tokenomics.BalanceSummary], *server.Response[server.ErrorResponse]) { + balance, err := s.tokenomicsRepository.GetBalanceSummary(contextWithHashCode(ctx, req), req.Data.UserID) + if err != nil { + err = errors.Wrapf(err, "failed to get user's balance summary for userID:%v", req.Data.UserID) + + return nil, server.Unexpected(err) + } + + return server.OK(balance), nil +} + +// GetBalanceHistory godoc +// +// @Schemes +// @Description Returns the balance history for the provided params. +// @Description If `startDate` is after `endDate`, we go backwards in time: I.E. today, yesterday, etc. +// @Description If `startDate` is before `endDate`, we go forwards in time: I.E. today, tomorrow, etc. +// @Tags Tokenomics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param userId path string true "ID of the user" +// @Param startDate query string false "The start date in RFC3339 or ISO8601 formats. Default is `now` in UTC." +// @Param endDate query string false "The start date in RFC3339 or ISO8601 formats. Default is `end of day, relative to startDate`." +// @Param tz query string false "The user's timezone. I.E. `+03:00`, `-1:30`. Default is UTC." +// @Param limit query uint64 false "max number of elements to return. Default is `24`." +// @Param offset query uint64 false "number of elements to skip before starting to fetch data" +// @Success 200 {array} tokenomics.BalanceHistoryEntry +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if not allowed" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics/{userId}/balance-history [GET]. +func (s *service) GetBalanceHistory( //nolint:gocritic,funlen // False negative. + ctx context.Context, + req *server.Request[GetBalanceHistoryArg, []*tokenomics.BalanceHistoryEntry], +) (*server.Response[[]*tokenomics.BalanceHistoryEntry], *server.Response[server.ErrorResponse]) { + const defaultLimit, maxLimit = 24, 1000 + if req.Data.Limit > maxLimit { + req.Data.Limit = maxLimit + } + if req.Data.Limit == 0 { + req.Data.Limit = defaultLimit + } + var startDate, endDate *time.Time + if req.Data.StartDate == nil { + startDate = time.Now() + } else { + startDate = time.New(*req.Data.StartDate) + } + if req.Data.EndDate == nil { + endDate = time.New(startDate.Add(-1 * users.NanosSinceMidnight(startDate))) + } else { + endDate = time.New(*req.Data.EndDate) + } + if req.Data.TZ == "" { + req.Data.TZ = "+00:00" + } + utcOffset, err := stdlibtime.ParseDuration(strings.Replace(req.Data.TZ+"m", ":", "h", 1)) + if err != nil { + return nil, server.UnprocessableEntity(errors.Wrapf(err, "invalid timezone:`%v`", req.Data.TZ), invalidPropertiesErrorCode) + } + hist, err := s.tokenomicsRepository.GetBalanceHistory(contextWithHashCode(ctx, req), req.Data.UserID, startDate, endDate, utcOffset, req.Data.Limit, req.Data.Offset) //nolint:lll // . + if err != nil { + err = errors.Wrapf(err, "failed to get user's balance history for userID:%v, data:%#v", req.Data.UserID, req.Data) + + return nil, server.Unexpected(err) + } + + return server.OK(&hist), nil +} + +// GetRankingSummary godoc +// +// @Schemes +// @Description Returns the ranking related information. +// @Tags Tokenomics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param userId path string true "ID of the user" +// @Success 200 {object} tokenomics.RankingSummary +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if hidden by the user" +// @Failure 404 {object} server.ErrorResponse "if not found" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /tokenomics/{userId}/ranking-summary [GET]. +func (s *service) GetRankingSummary( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetRankingSummaryArg, tokenomics.RankingSummary], +) (*server.Response[tokenomics.RankingSummary], *server.Response[server.ErrorResponse]) { + ranking, err := s.tokenomicsRepository.GetRankingSummary(contextWithHashCode(ctx, req), req.Data.UserID) + if err != nil { + err = errors.Wrapf(err, "failed to get user's ranking summary for userID:%v", req.Data.UserID) + if errors.Is(err, tokenomics.ErrRelationNotFound) { + return nil, server.NotFound(err, userNotFoundErrorCode) + } + if errors.Is(err, tokenomics.ErrGlobalRankHidden) { + return nil, server.ForbiddenWithCode(err, globalRankHiddenErrorCode) + } + + return nil, server.Unexpected(err) + } + + return server.OK(ranking), nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cde6567 --- /dev/null +++ b/go.mod @@ -0,0 +1,145 @@ +module github.com/ice-blockchain/freezer + +go 1.20 + +require ( + github.com/cenkalti/backoff/v4 v4.2.0 + github.com/goccy/go-json v0.10.2 + github.com/google/uuid v1.3.0 + github.com/hashicorp/go-multierror v1.1.1 + github.com/ice-blockchain/eskimo v1.101.0 + github.com/ice-blockchain/go-tarantool-client v0.0.0-20230322193140-81ac2079df0c + github.com/ice-blockchain/wintr v1.101.0 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.8.2 + github.com/swaggo/swag v1.8.11 + github.com/testcontainers/testcontainers-go v0.19.0 +) + +require ( + cloud.google.com/go v0.110.0 // indirect + cloud.google.com/go/compute v1.19.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/firestore v1.9.0 // indirect + cloud.google.com/go/iam v0.13.0 // indirect + cloud.google.com/go/longrunning v0.4.1 // indirect + cloud.google.com/go/storage v1.30.1 // indirect + cosmossdk.io/math v1.0.0-rc.0 // indirect + firebase.google.com/go/v4 v4.10.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/MicahParks/keyfunc v1.9.0 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Microsoft/hcsshim v0.10.0-rc.7 // indirect + github.com/bytedance/sonic v1.8.5 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/containerd/containerd v1.7.0 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v23.0.1+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.8 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.12.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/goccy/go-reflect v1.2.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect + github.com/googleapis/gax-go/v2 v2.8.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.14 // indirect + github.com/imroc/req/v3 v3.33.1 // indirect + github.com/ip2location/ip2location-go/v9 v9.5.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.2 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/sys/mount v0.3.3 // indirect + github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/onsi/ginkgo/v2 v2.9.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect + github.com/opencontainers/runc v1.1.4 // indirect + github.com/pelletier/go-toml/v2 v2.0.7 // indirect + github.com/pierrec/lz4/v4 v4.1.17 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-19 v0.3.0 // indirect + github.com/quic-go/qtls-go1-20 v0.2.0 // indirect + github.com/quic-go/quic-go v0.33.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/rs/zerolog v1.29.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.15.0 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/swaggo/files v1.0.1 // indirect + github.com/swaggo/gin-swagger v1.5.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/twmb/franz-go v1.13.1 // indirect + github.com/twmb/franz-go/pkg/kadm v1.8.0 // indirect + github.com/twmb/franz-go/pkg/kmsg v1.4.0 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.7.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/api v0.114.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine/v2 v2.0.2 // indirect + google.golang.org/genproto v0.0.0-20230322174352-cde4c949918d // indirect + google.golang.org/grpc v1.54.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace ( + github.com/containerd/containerd => github.com/containerd/containerd v1.6.19 + github.com/docker/docker => github.com/docker/docker v20.10.3+incompatible + github.com/testcontainers/testcontainers-go => github.com/testcontainers/testcontainers-go v0.15.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bb55a9c --- /dev/null +++ b/go.sum @@ -0,0 +1,863 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cosmossdk.io/math v1.0.0-rc.0 h1:ml46ukocrAAoBpYKMidF0R2tQJ1Uxfns0yH8wqgMAFc= +cosmossdk.io/math v1.0.0-rc.0/go.mod h1:Ygz4wBHrgc7g0N+8+MrnTfS9LLn9aaTGa9hKopuym5k= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4= +firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= +github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/Microsoft/hcsshim v0.10.0-rc.7 h1:HBytQPxcv8Oy4244zbQbe6hnOnx544eL5QPUqhJldz8= +github.com/Microsoft/hcsshim v0.10.0-rc.7/go.mod h1:ILuwjA+kNW+MrN/w5un7n3mTqkwsFu4Bp05/okFUZlE= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.8.5 h1:kjX0/vo5acEQ/sinD/18SkA/lDDUk23F0RcaHvI7omc= +github.com/bytedance/sonic v1.8.5/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/containerd v1.6.19 h1:F0qgQPrG0P2JPgwpxWxYavrVeXAG0ezUIB9Z/4FTUAU= +github.com/containerd/containerd v1.6.19/go.mod h1:HZCDMn4v/Xl2579/MvtOC2M206i+JJ6VxFWU/NetrGY= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.3+incompatible h1:+HS4XO73J41FpA260ztGujJ+0WibrA2TPJEnWNSyGNE= +github.com/docker/docker v20.10.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= +github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU= +github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= +github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms= +github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230309165930-d61513b1440d h1:um9/pc7tKMINFfP1eE7Wv6PRGXlcCSJkVajF7KJw3uQ= +github.com/google/pprof v0.0.0-20230309165930-d61513b1440d/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc= +github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ice-blockchain/eskimo v1.101.0 h1:ONIieQZCJcRdxEuNFS9DXYbp2Gs0bqX6YutACtLErjo= +github.com/ice-blockchain/eskimo v1.101.0/go.mod h1:OXEZa2wIbWWofnH0FFa2Za/oEBS9h48QWj6nVJF/JL0= +github.com/ice-blockchain/go-tarantool-client v0.0.0-20230322193140-81ac2079df0c h1:c0fZ+DTu1zlEggEGckFzdGx0tuWQDPf4PjfAbQkHFAU= +github.com/ice-blockchain/go-tarantool-client v0.0.0-20230322193140-81ac2079df0c/go.mod h1:ZsQU7i3mxhgBBu43Oev7WPFbIjP4TniN/b1UPNGbrq8= +github.com/ice-blockchain/wintr v1.101.0 h1:84dJ+1TnlU4jLPnGO/om2BgigvOXhQhxDDNX1lGh/MY= +github.com/ice-blockchain/wintr v1.101.0/go.mod h1:YElO+8IEByLZPSr5/zSmI9H49Dg5GnjcouAgUv2zEC4= +github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo= +github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/imroc/req/v3 v3.33.1 h1:BZnyl+K0hXcJlZBHY2CqbPgmVc1pPJDzjn6aJfB6shI= +github.com/imroc/req/v3 v3.33.1/go.mod h1:cZ+7C3L/AYOr4tLGG16hZF90F1WzAdAdzt1xFSlizXY= +github.com/ip2location/ip2location-go/v9 v9.5.0 h1:7gqKncm4MhBrpJIK0PmV8o6Bf8YbbSAPjORzyjAv1iM= +github.com/ip2location/ip2location-go/v9 v9.5.0/go.mod h1:s5SV6YZL10TpfPpXw//7fEJC65G/yH7Oh+Tjq9JcQEQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= +github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4= +github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= +github.com/moby/sys/mount v0.3.3/go.mod h1:PBaEorSNTLG5t/+4EgukEQVlAvVEc6ZjTySwKdqp5K0= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= +github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= +github.com/onsi/gomega v1.27.3 h1:5VwIwnBY3vbBDOJrNtA4rVdiTZCsq9B5F12pvy1Drmk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opencontainers/runc v1.1.4 h1:nRCz/8sKg6K6jgYAFLDlXzPeITBZJyX28DBVhWD+5dg= +github.com/opencontainers/runc v1.1.4/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= +github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= +github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/qtls-go1-19 v0.3.0 h1:aUBoQdpHzUWtPw5tQZbsD2GnrWCNu7/RIX1PtqGeLYY= +github.com/quic-go/qtls-go1-19 v0.3.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.2.0 h1:jUHn+obJ6WI5JudqBO0Iy1ra5Vh5vsitQ1gXQvkmN+E= +github.com/quic-go/qtls-go1-20 v0.2.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= +github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= +github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= +github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= +github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= +github.com/swaggo/swag v1.8.11 h1:Fp1dNNtDvbCf+8kvehZbHQnlF6AxHGjmw6H/xAMrZfY= +github.com/swaggo/swag v1.8.11/go.mod h1:2GXgpNI9iy5OdsYWu8zXfRAGnOAPxYxTWTyM0XOTYZQ= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/testcontainers/testcontainers-go v0.15.0 h1:3Ex7PUGFv0b2bBsdOv6R42+SK2qoZnWBd21LvZYhUtQ= +github.com/testcontainers/testcontainers-go v0.15.0/go.mod h1:PkohMRH2X8Hib0IWtifVexDfLPVT+tb5E9hsf7cW12w= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/twmb/franz-go v1.13.1 h1:7dDuAjJItbZRjgmMzYzCfJtRMpVKfQ57yIG8SkZz24M= +github.com/twmb/franz-go v1.13.1/go.mod h1:jm/FtYxmhxDTN0gNSb26XaJY0irdSVcsckLiR5tQNMk= +github.com/twmb/franz-go/pkg/kadm v1.8.0 h1:vvKwZpxYn+VmM32v4mKkecHLKavZW+HcYLRKKxly5ZY= +github.com/twmb/franz-go/pkg/kadm v1.8.0/go.mod h1:qUSM7pxoMCU1UNu5H4USE64ODcVmeG9LS96mysv1nu8= +github.com/twmb/franz-go/pkg/kmsg v1.4.0 h1:tbp9hxU6m8qZhQTlpGiaIJOm4BXix5lsuEZ7K00dF0s= +github.com/twmb/franz-go/pkg/kmsg v1.4.0/go.mod h1:SxG/xJKhgPu25SamAq0rrucfp7lbzCpEXOC+vH/ELrY= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk= +google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20230322174352-cde4c949918d h1:OE8TncEeAei3Tehf/P/Jdt/K+8GnTUrRY6wzYpbCes4= +google.golang.org/genproto v0.0.0-20230322174352-cde4c949918d/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/local.go b/local.go new file mode 100644 index 0000000..19e5c57 --- /dev/null +++ b/local.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: ice License 1.0 + +package main + +import ( + "flag" + + "github.com/ice-blockchain/freezer/tokenomics/fixture" + "github.com/ice-blockchain/freezer/tokenomics/seeding" + serverauthfixture "github.com/ice-blockchain/wintr/auth/fixture" + "github.com/ice-blockchain/wintr/log" +) + +//nolint:gochecknoglobals // Because those are flags +var ( + generateAuth = flag.String("generateAuth", "", "generate a new auth for a random user, with the specified role") + startSeeding = flag.Bool("startSeeding", false, "whether to start seeding a remote database or not") + startLocalType = flag.String("type", "all", "the strategy to use to spin up the local environment") +) + +func main() { + flag.Parse() + if generateAuth != nil && *generateAuth != "" { + userID, token := testingAuthorization(*generateAuth) + log.Info("UserID") + log.Info("=================================================================================") + log.Info(userID) + log.Info("Authorization Bearer Token") + log.Info("=================================================================================") + log.Info(token) + + return + } + if *startSeeding { + seeding.StartSeeding() + + return + } + + fixture.StartLocalTestEnvironment(fixture.StartLocalTestEnvironmentType(*startLocalType)) +} + +func testingAuthorization(role string) (userID, token string) { + return serverauthfixture.CreateUser(role) +} diff --git a/tokenomics/.testdata/application.yaml b/tokenomics/.testdata/application.yaml new file mode 100644 index 0000000..620c193 --- /dev/null +++ b/tokenomics/.testdata/application.yaml @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: ice License 1.0 + +development: true +logger: + encoder: console + level: debug +tokenomics: &tokenomics + db: &tokenomicsDatabase + urls: + - localhost:3302 + user: admin + password: pass + messageBroker: &tokenomicsMessageBroker + consumerGroup: tokenomics-testing + createTopics: true + urls: + - localhost:9093 + topics: &tokenomicsMessageBrokerTopics + - name: freezer-health-check + partitions: 1 + replicationFactor: 1 + retention: 1000h + - name: adoption-table + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: mining-sessions-table + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: mining-rates-table + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: balances-table + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: add-balance-commands + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: pre-stakings-table + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: available-daily-bonuses + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: started-days-off + partitions: 10 + replicationFactor: 1 + retention: 10s + - name: balance-recalculation-trigger-stream + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: mining-rates-recalculation-trigger-stream + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: blockchain-balance-synchronization-trigger-stream + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: extra-bonus-processing-trigger-stream + partitions: 10 + replicationFactor: 1 + retention: 1000h + ### The next topics are not owned by this service, but are needed to be created for the local/test environment. + - name: users-table + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: global-table + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: viewed-news + partitions: 10 + replicationFactor: 1 + retention: 1000h + - name: user-device-metadata-table + partitions: 10 + replicationFactor: 1 + retention: 1000h + consumingTopics: + - name: users-table + - name: global-table + - name: mining-sessions-table + - name: add-balance-commands + - name: viewed-news + - name: user-device-metadata-table + - name: balance-recalculation-trigger-stream + oneGoroutinePerPartition: true + - name: mining-rates-recalculation-trigger-stream + oneGoroutinePerPartition: true + - name: blockchain-balance-synchronization-trigger-stream + oneGoroutinePerPartition: true + - name: extra-bonus-processing-trigger-stream + oneGoroutinePerPartition: true + wintr/multimedia/picture: + urlDownload: https://ice-staging.b-cdn.net/profile + workerCount: 10 + referralBonusMiningRates: + t0: 25 + t1: 25 + t2: 5 + rollbackNegativeMining: + aggressiveDegradationStartsAfter: 3m + available: + after: 1m + until: 10m + miningSessionDuration: + min: 1m + max: 2m + warnAboutExpirationAfter: 100s + consecutiveNaturalMiningSessionsRequiredFor1ExtraFreeArtificialMiningSession: + min: 12 + max: 6 + globalAggregationInterval: + parent: 60m + child: 1m + adoptionMilestoneSwitch: + duration: 10s + consecutiveDurationsRequired: 2 + activeUserMilestones: + - 2 + - 4 + - 6 + - 8 + - 10 + extraBonuses: + duration: 24m + utcOffsetDuration: 6s + claimWindow: 1m + delayedClaimPenaltyWindow: 15s + availabilityWindow: 10m + timeToAvailabilityWindow: 10m + flatValues: + - 2 + - 4 + - 6 + - 8 + - 10 + newsSeenValues: + - 0 + - 6 + - 15 + - 54 + - 90 + miningStreakValues: + - 0 + - 2 + - 5 + - 9 + - 20 +tokenomics_test: + <<: *tokenomics + messageBroker: + <<: *tokenomicsMessageBroker + consumingTopics: *tokenomicsMessageBrokerTopics + db: + <<: *tokenomicsDatabase + schemaPath: tokenomics/DDL.lua diff --git a/tokenomics/DDL.lua b/tokenomics/DDL.lua new file mode 100644 index 0000000..e4b267f --- /dev/null +++ b/tokenomics/DDL.lua @@ -0,0 +1,209 @@ +-- SPDX-License-Identifier: ice License 1.0 +--************************************************************************************************************************************ +-- pre_staking_bonuses +box.execute([[CREATE TABLE IF NOT EXISTS pre_staking_bonuses ( + years UNSIGNED PRIMARY KEY, + bonus UNSIGNED NOT NULL CHECK (bonus > 0) + ) + WITH ENGINE = 'memtx';]]) +box.execute([[INSERT INTO pre_staking_bonuses (years, bonus) + VALUES (1, 35), + (2, 70), + (3, 115), + (4, 170), + (5, 250);]]) +--************************************************************************************************************************************ +-- extra_bonuses +box.execute([[CREATE TABLE IF NOT EXISTS extra_bonuses ( + ix UNSIGNED PRIMARY KEY, + bonus UNSIGNED NOT NULL DEFAULT 0 + ) + WITH ENGINE = 'memtx';]]) +box.execute([[INSERT INTO extra_bonuses (ix, bonus) VALUES %[3]v;]]) +--************************************************************************************************************************************ +-- global +box.execute([[CREATE TABLE IF NOT EXISTS global ( + key STRING PRIMARY KEY, + value SCALAR NOT NULL + ) + WITH ENGINE = 'memtx';]]) +--************************************************************************************************************************************ +-- extra_bonus_start_date +box.execute([[CREATE TABLE IF NOT EXISTS extra_bonus_start_date ( + key UNSIGNED NOT NULL PRIMARY KEY, + value UNSIGNED NOT NULL + ) + WITH ENGINE = 'memtx';]]) +box.execute([[INSERT INTO extra_bonus_start_date (key, value) VALUES (0,%[4]v);]]) +--************************************************************************************************************************************ +-- adoption +box.execute([[CREATE TABLE IF NOT EXISTS adoption ( + achieved_at UNSIGNED, + base_mining_rate STRING NOT NULL, + milestone UNSIGNED PRIMARY KEY, + total_active_users UNSIGNED NOT NULL + ) + WITH ENGINE = 'memtx';]]) +box.execute([[INSERT INTO adoption (milestone, total_active_users, base_mining_rate, achieved_at) + VALUES (1, 0, '16000000000', %[1]v), + (2, %[5]v, '8000000000', null), + (3, %[6]v, '4000000000', null), + (4, %[7]v, '2000000000', null), + (5, %[8]v, '1000000000', null), + (6, %[9]v, '500000000', null);]]) +--************************************************************************************************************************************ +-- users +box.execute([[CREATE TABLE IF NOT EXISTS users ( + created_at UNSIGNED NOT NULL, + updated_at UNSIGNED NOT NULL, + rollback_used_at UNSIGNED, + last_natural_mining_started_at UNSIGNED, + last_mining_started_at UNSIGNED, + last_mining_ended_at UNSIGNED, + previous_mining_started_at UNSIGNED, + previous_mining_ended_at UNSIGNED, + last_free_mining_session_awarded_at UNSIGNED, + user_id STRING PRIMARY KEY, + referred_by STRING, + username STRING, + first_name STRING, + last_name STRING, + profile_picture_name STRING, + mining_blockchain_account_address STRING, + blockchain_account_address STRING, + hash_code UNSIGNED NOT NULL, + hide_ranking BOOLEAN NOT NULL DEFAULT FALSE, + verified BOOLEAN NOT NULL DEFAULT FALSE + ) + WITH ENGINE = 'memtx';]]) +box.execute([[CREATE INDEX IF NOT EXISTS users_referred_by_idx ON users (referred_by);]]) +box.execute([[CREATE INDEX IF NOT EXISTS top_miners_lookup_idx ON users (username,first_name,last_name);]]) +--************************************************************************************************************************************ +-- balances +box.execute([[CREATE TABLE IF NOT EXISTS balances ( + amount STRING NOT NULL DEFAULT '0', + amount_w0 UNSIGNED NOT NULL DEFAULT 0, + amount_w1 UNSIGNED NOT NULL DEFAULT 0, + amount_w2 UNSIGNED NOT NULL DEFAULT 0, + amount_w3 UNSIGNED NOT NULL DEFAULT 0, + user_id STRING NOT NULL PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE + ) + WITH ENGINE = 'memtx';]]) +box.execute([[CREATE INDEX IF NOT EXISTS balances_amount_words_ix ON balances (amount_w3, amount_w2, amount_w1, amount_w0);]]) +--************************************************************************************************************************************ +-- processed_add_balance_commands +box.execute([[CREATE TABLE IF NOT EXISTS processed_add_balance_commands ( + user_id STRING NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + key STRING NOT NULL, + PRIMARY KEY (user_id, key) + ) + WITH ENGINE = 'vinyl';]]) +--************************************************************************************************************************************ +-- processed_seen_news +box.execute([[CREATE TABLE IF NOT EXISTS processed_seen_news ( + user_id STRING NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + news_id STRING NOT NULL, + PRIMARY KEY (user_id, news_id) + ) + WITH ENGINE = 'vinyl';]]) +--************************************************************************************************************************************ +-- mining_sessions_dlq +for worker_index=0,%[2]v do + box.execute([[CREATE TABLE IF NOT EXISTS mining_sessions_dlq_]] .. worker_index .. [[ ( + id STRING NOT NULL PRIMARY KEY, + user_id STRING NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + message STRING NOT NULL + ) + WITH ENGINE = 'vinyl';]]) +end +--************************************************************************************************************************************ +-- extra_bonuses +for worker_index=0,%[2]v do + box.execute([[CREATE TABLE IF NOT EXISTS extra_bonuses_]] .. worker_index .. [[ ( + extra_bonus_index UNSIGNED NOT NULL PRIMARY KEY REFERENCES extra_bonuses(ix) ON DELETE CASCADE, + offset UNSIGNED NOT NULL DEFAULT 0 + ) + WITH ENGINE = 'memtx';]]) +end +--************************************************************************************************************************************ +-- pre_stakings +for worker_index=0,%[2]v do + box.execute([[CREATE TABLE IF NOT EXISTS pre_stakings_]] .. worker_index .. [[ ( + created_at UNSIGNED NOT NULL, + user_id STRING NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + years UNSIGNED NOT NULL REFERENCES pre_staking_bonuses(years), + allocation UNSIGNED NOT NULL CHECK (allocation > 0 AND allocation <= 100), + PRIMARY KEY (user_id, years, allocation) + ) + WITH ENGINE = 'memtx';]]) + box.execute([[CREATE INDEX IF NOT EXISTS pre_stakings_]] .. worker_index .. [[_years_idx ON pre_stakings_]] .. worker_index .. [[ (years);]]) +end +--************************************************************************************************************************************ +-- balance_recalculation_workers +for worker_index=0,%[2]v do + box.execute([[CREATE TABLE IF NOT EXISTS balance_recalculation_worker_]] .. worker_index .. [[ + ( + last_iteration_finished_at UNSIGNED, + last_mining_started_at UNSIGNED, + last_mining_ended_at UNSIGNED, + enabled BOOLEAN NOT NULL DEFAULT FALSE, + user_id STRING PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE + ) + WITH ENGINE = 'memtx';]]) + box.execute([[CREATE INDEX IF NOT EXISTS balance_recalculation_worker_]] .. worker_index .. [[_iterator_ix ON balance_recalculation_worker_]] .. worker_index .. [[ (enabled,last_iteration_finished_at);]]) + box.execute([[CREATE INDEX IF NOT EXISTS balance_recalculation_worker_]] .. worker_index .. [[_iterator2_ix ON balance_recalculation_worker_]] .. worker_index .. [[ (user_id,last_iteration_finished_at);]]) +end +--************************************************************************************************************************************ +-- mining_rates_recalculation_workers +for worker_index=0,%[2]v do + box.execute([[CREATE TABLE IF NOT EXISTS mining_rates_recalculation_worker_]] .. worker_index .. [[ + ( + last_iteration_finished_at UNSIGNED, + user_id STRING PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE + ) + WITH ENGINE = 'memtx';]]) + box.execute([[CREATE INDEX IF NOT EXISTS mining_rates_recalculation_worker_]] .. worker_index .. [[_last_iteration_finished_at_ix ON mining_rates_recalculation_worker_]] .. worker_index .. [[ (last_iteration_finished_at);]]) +end +--************************************************************************************************************************************ +-- blockchain_balance_synchronization_workers +for worker_index=0,%[2]v do + box.execute([[CREATE TABLE IF NOT EXISTS blockchain_balance_synchronization_worker_]] .. worker_index .. [[ + ( + last_iteration_finished_at UNSIGNED, + mining_blockchain_account_address STRING, + user_id STRING PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE + ) + WITH ENGINE = 'memtx';]]) + box.execute([[CREATE INDEX IF NOT EXISTS blockchain_balance_synchronization_worker_]] .. worker_index .. [[_last_iteration_finished_at_ix ON blockchain_balance_synchronization_worker_]] .. worker_index .. [[ (last_iteration_finished_at);]]) +end +--************************************************************************************************************************************ +-- extra_bonus_processing_workers +for worker_index=0,%[2]v do + box.execute([[CREATE TABLE IF NOT EXISTS extra_bonus_processing_worker_]] .. worker_index .. [[ + ( + extra_bonus_started_at UNSIGNED, + extra_bonus_ended_at UNSIGNED, + user_id STRING NOT NULL PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE, + utc_offset INT NOT NULL DEFAULT 0, + news_seen UNSIGNED NOT NULL DEFAULT 0, + extra_bonus UNSIGNED NOT NULL DEFAULT 0, + last_extra_bonus_index_notified UNSIGNED REFERENCES extra_bonuses(ix) ON DELETE SET NULL + ) + WITH ENGINE = 'memtx';]]) + box.execute([[CREATE INDEX IF NOT EXISTS extra_bonus_processing_worker_]] .. worker_index .. [[_iterator_ix ON extra_bonus_processing_worker_]] .. worker_index .. [[ (last_extra_bonus_index_notified);]]) +end +--************************************************************************************************************************************ +-- balances +for worker_index=0,%[2]v do + box.execute([[CREATE TABLE IF NOT EXISTS balances_]] .. worker_index .. [[ + ( + updated_at UNSIGNED NOT NULL, + amount STRING NOT NULL DEFAULT '0', + user_id STRING NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + type_detail STRING NOT NULL DEFAULT '', + type UNSIGNED NOT NULL, + negative BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (user_id, negative, type, type_detail) + ) + WITH ENGINE = 'memtx';]]) +end \ No newline at end of file diff --git a/tokenomics/adoption.go b/tokenomics/adoption.go new file mode 100644 index 0000000..5bdcb40 --- /dev/null +++ b/tokenomics/adoption.go @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + "strconv" + "strings" + stdlibtime "time" + + "github.com/goccy/go-json" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + + "github.com/ice-blockchain/go-tarantool-client" + "github.com/ice-blockchain/wintr/coin" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) GetAdoptionSummary(ctx context.Context) (as *AdoptionSummary, err error) { + if as = new(AdoptionSummary); ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "context failed") + } + key := r.totalActiveUsersGlobalParentKey(time.Now().Time) + if as.TotalActiveUsers, err = r.getGlobalUnsignedValue(ctx, key); err != nil && !errors.Is(err, storage.ErrNotFound) { + return nil, errors.Wrapf(err, "failed to get totalActiveUsers getGlobalUnsignedValue for key:%v", key) + } + if as.Milestones, err = getAllAdoptions[coin.ICE](ctx, r.db); err != nil { + return nil, errors.Wrapf(err, "failed to get all adoption milestones") + } + + return +} + +func (r *repository) totalActiveUsersGlobalParentKey(date *stdlibtime.Time) string { + return fmt.Sprintf("%v_%v", totalActiveUsersGlobalKey, date.Format(r.cfg.globalAggregationIntervalParentDateFormat())) +} + +func (r *repository) totalActiveUsersGlobalChildKey(date *stdlibtime.Time) string { + return fmt.Sprintf("%v_%v", totalActiveUsersGlobalKey, date.Format(r.cfg.globalAggregationIntervalChildDateFormat())) +} + +func (r *repository) trySwitchToNextAdoption(ctx context.Context) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "context failed") + } + nextAdoption, err := r.getNextAdoption(ctx) + if err != nil { + return errors.Wrap(err, "failed to try to get next adoption") + } + if nextAdoption == nil { + return nil + } + if err = r.switchToNextAdoption(ctx, nextAdoption); err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil // This is a concurrency check. Multiple goroutines will try to update it, but only the 1st will succeed. + } + + return errors.Wrap(err, "failed to try to get next adoption") + } + + if err = r.notifyAdoptionChange(ctx, nextAdoption); err != nil { + revertCtx, revertCancel := context.WithTimeout(context.Background(), requestDeadline) + defer revertCancel() + + return multierror.Append( + errors.Wrapf(err, "failed notifyAdoptionChange for:%#v", nextAdoption), + errors.Wrapf(r.revertSwitchToNextAdoption(revertCtx, nextAdoption), //nolint:contextcheck // It might be cancelled. + "failed to revertSwitchToNextAdoption for:%#v", nextAdoption)) + } + + return nil +} + +func (r *repository) notifyAdoptionChange(ctx context.Context, nextAdoption *Adoption[coin.ICEFlake]) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "context failed") + } + adoption, err := r.getAdoption(ctx, nextAdoption.Milestone-1) + if err != nil && !errors.Is(err, storage.ErrNotFound) { + return errors.Wrapf(err, "failed to get adoption for milestone:%v", nextAdoption.Milestone-1) + } + snapshot := &AdoptionSnapshot{Adoption: nextAdoption, Before: adoption} + if err = r.sendAdoptionSnapshotMessage(ctx, snapshot); err != nil { + return errors.Wrapf(err, "failed to sendAdoptionSnapshotMessage: %#v", snapshot) + } + + return nil +} + +func (r *repository) getAdoption(ctx context.Context, milestone uint64) (*Adoption[coin.ICEFlake], error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "context failed") + } + resp := new(Adoption[coin.ICEFlake]) + if err := r.db.GetTyped("ADOPTION", "pk_unnamed_ADOPTION_1", tarantool.UintKey{I: uint(milestone)}, resp); err != nil { + return nil, errors.Wrapf(err, "failed to get the adoption by milestone:%v", milestone) + } + if resp.Milestone == 0 { + return nil, storage.ErrNotFound + } + + return resp, nil +} + +func getAllAdoptions[DENOM coin.ICEFlake | coin.ICE](ctx context.Context, db tarantool.Connector) ([]*Adoption[DENOM], error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "context failed") + } + resp := make([]*Adoption[DENOM], 0, lastAdoptionMilestone) + if err := db.SelectTyped("ADOPTION", "pk_unnamed_ADOPTION_1", 0, lastAdoptionMilestone, tarantool.IterAll, []any{}, &resp); err != nil { + return nil, errors.Wrap(err, "failed to select for all adoptions") + } + + return resp, nil +} + +func (r *repository) getCurrentAdoption(ctx context.Context) (*Adoption[coin.ICEFlake], error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "context failed") + } + resp := make([]*Adoption[coin.ICEFlake], 0, 1) + if err := r.db.PrepareExecuteTyped(currentAdoptionSQL(), map[string]any{}, &resp); err != nil { + return nil, errors.Wrap(err, "failed to select for the current adoption") + } + if len(resp) == 0 || resp[0] == nil || resp[0].Milestone == 0 { //nolint:revive // Nope. + return nil, storage.ErrNotFound // Should never happen. + } + + return resp[0], nil +} + +func currentAdoptionSQL() string { + return `SELECT achieved_at, + base_mining_rate, + MAX(milestone) AS milestone, + total_active_users + FROM adoption + WHERE achieved_at IS NOT NULL` +} + +func (r *repository) getNextAdoption(ctx context.Context) (*Adoption[coin.ICEFlake], error) { //nolint:funlen // Alot of SQL & mappings. + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "context failed") + } + var ( + consecutiveDurationsRequired = stdlibtime.Duration(r.cfg.AdoptionMilestoneSwitch.ConsecutiveDurationsRequired) + keyParams = make([]string, 0, consecutiveDurationsRequired) + params = make(map[string]any, cap(keyParams)+1+1) + now = time.Now() + ) + params["expected_consecutive_durations"] = consecutiveDurationsRequired + params["minimum_time_for_the_previous_adoption_to_be_achieved"] = time.New(now.Add(-consecutiveDurationsRequired * r.cfg.AdoptionMilestoneSwitch.Duration)) + for duration := stdlibtime.Duration(0); duration < consecutiveDurationsRequired; duration++ { + relativeTime := now.Add(-duration * r.cfg.AdoptionMilestoneSwitch.Duration) + params[fmt.Sprintf("total_active_per_duration%v_key", duration)] = r.totalActiveUsersGlobalChildKey(&relativeTime) + keyParams = append(keyParams, fmt.Sprintf(":total_active_per_duration%v_key", duration)) + } + sql := fmt.Sprintf(`SELECT x.achieved_at, + x.base_mining_rate, + x.milestone, + x.total_active_users + FROM (SELECT next_adoption.*, + COUNT(g.key) AS consecutive_durations + FROM global g + JOIN (%[2]v) current_adoption + JOIN adoption next_adoption + ON g.key IN (%[1]v) + AND next_adoption.milestone = current_adoption.milestone + 1 + AND current_adoption.achieved_at < :minimum_time_for_the_previous_adoption_to_be_achieved + AND CAST(g.value AS UNSIGNED) >= next_adoption.total_active_users) x + WHERE x.consecutive_durations == :expected_consecutive_durations + AND x.achieved_at IS NULL`, strings.Join(keyParams, ","), currentAdoptionSQL()) + resp := make([]*Adoption[coin.ICEFlake], 0, 1) + if err := r.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return nil, errors.Wrap(err, "failed to select if the next adoption is achieved") + } + if len(resp) == 0 || resp[0] == nil || resp[0].Milestone == 0 { //nolint:revive // Nope. + return nil, nil //nolint:nilnil // Nope. + } + resp[0].AchievedAt = now + + return resp[0], nil +} + +func (r *repository) switchToNextAdoption(ctx context.Context, nextAdoption *Adoption[coin.ICEFlake]) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "context failed") + } + sql := `UPDATE adoption + SET achieved_at = :achieved_at + WHERE milestone = :milestone + AND achieved_at IS NULL` + params := make(map[string]any, 1+1) + params["milestone"] = nextAdoption.Milestone + params["achieved_at"] = nextAdoption.AchievedAt + + return errors.Wrapf(storage.CheckSQLDMLErr(r.db.PrepareExecute(sql, params)), + "failed to update the next adoption to switch to it, params:%#v", params) +} + +func (r *repository) revertSwitchToNextAdoption(ctx context.Context, nextAdoption *Adoption[coin.ICEFlake]) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "context failed") + } + sql := `UPDATE adoption + SET achieved_at = NULL + WHERE milestone = :milestone + AND achieved_at IS NOT NULL AND achieved_at = :achieved_at` + params := make(map[string]any, 1+1) + params["milestone"] = nextAdoption.Milestone + params["achieved_at"] = nextAdoption.AchievedAt + + return errors.Wrapf(storage.CheckSQLDMLErr(r.db.PrepareExecute(sql, params)), + "failed to revert to update the next adoption to switch to it, params:%#v", params) +} + +func (r *repository) mustNotifyCurrentAdoption(ctx context.Context) { + adoption, err := r.getCurrentAdoption(ctx) + log.Panic(errors.Wrapf(err, "failed to get getCurrentAdoption")) //nolint:revive // Intended. + snapshot := &AdoptionSnapshot{Adoption: adoption, Before: adoption} + log.Panic(errors.Wrapf(r.sendAdoptionSnapshotMessage(ctx, snapshot), "failed to sendAdoptionSnapshotMessage: %#v", snapshot)) +} + +func (r *repository) sendAdoptionSnapshotMessage(ctx context.Context, snapshot *AdoptionSnapshot) error { + valueBytes, err := json.MarshalContext(ctx, snapshot) + if err != nil { + return errors.Wrapf(err, "failed to marshal %#v", snapshot) + } + + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: strconv.FormatUint(snapshot.Milestone, 10), + Topic: r.cfg.MessageBroker.Topics[1].Name, + Value: valueBytes, + } + + responder := make(chan error, 1) + defer close(responder) + r.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send %v message to broker, msg:%#v", msg.Topic, snapshot) +} diff --git a/tokenomics/balance.go b/tokenomics/balance.go new file mode 100644 index 0000000..39114c0 --- /dev/null +++ b/tokenomics/balance.go @@ -0,0 +1,571 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + "sort" + "strings" + stdlibtime "time" + + "github.com/cenkalti/backoff/v4" + "github.com/goccy/go-json" + "github.com/pkg/errors" + + "github.com/ice-blockchain/wintr/coin" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) GetBalanceSummary( //nolint:funlen,gocognit // Better to be grouped together. + ctx context.Context, userID string, +) (*BalanceSummary, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(` +SELECT b.*, + x.pre_staking_allocation, + st_b.bonus AS pre_staking_bonus, + bal_worker.last_iteration_finished_at IS NOT NULL AND bal_worker.last_mining_ended_at IS NOT NULL +FROM (SELECT MAX(st.years) AS pre_staking_years, + MAX(st.allocation) AS pre_staking_allocation, + u.user_id, + u.referred_by + FROM users u + LEFT JOIN pre_stakings_%[1]v st + ON st.user_id = u.user_id + WHERE u.user_id = :user_id + GROUP BY u.user_id + ) x + LEFT JOIN pre_staking_bonuses st_b + ON st_b.years = x.pre_staking_years + JOIN balance_recalculation_worker_%[1]v bal_worker + ON bal_worker.user_id = x.user_id + LEFT JOIN balances_%[1]v b + ON b.user_id = x.user_id + AND b.negative = FALSE + AND b.type = %[2]v + AND b.type_detail IN ('','%[3]v_' || x.referred_by,'%[4]v','%[5]v')`, r.workerIndex(ctx), totalNoPreStakingBonusBalanceType, t0BalanceTypeDetail, t1BalanceTypeDetail, t2BalanceTypeDetail) //nolint:lll // . + params := make(map[string]any, 1) + params["user_id"] = userID + type B = balance + resp := make([]*struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:tagliatelle,revive,nosnakecase // To insert we need asArray + *B + PreStakingAllocation, PreStakingBonus uint64 + BalanceWorkerStarted bool + }, 0, 1+1+1) + if err := r.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return nil, errors.Wrapf(err, "failed to select user's balances for user_id:%v", userID) + } + total, totalNoPreStakingBonus, t1, t2, standard, preStaking := coin.ZeroICEFlakes(), coin.ZeroICEFlakes(), coin.ZeroICEFlakes(), coin.ZeroICEFlakes(), coin.ZeroICEFlakes(), coin.ZeroICEFlakes() //nolint:lll // . + for _, row := range resp { + if row.B == nil || row.B.Amount == nil { + continue + } + standardAmount := row.Amount. + MultiplyUint64(percentage100 - row.PreStakingAllocation). + DivideUint64(percentage100) + preStakingAmount := row.Amount. + MultiplyUint64(row.PreStakingAllocation * (row.PreStakingBonus + percentage100)). + DivideUint64(percentage100 * percentage100) + switch row.TypeDetail { + case t1BalanceTypeDetail: + t1 = t1.Add(standardAmount.Add(preStakingAmount)) + case t2BalanceTypeDetail: + t2 = standardAmount.Add(preStakingAmount) + default: + if strings.HasPrefix(row.TypeDetail, t0BalanceTypeDetail) { + t1 = t1.Add(standardAmount.Add(preStakingAmount)) + } + } + standard = standard.Add(standardAmount) + preStaking = preStaking.Add(preStakingAmount) + total = total.Add(standardAmount.Add(preStakingAmount)) + totalNoPreStakingBonus = totalNoPreStakingBonus.Add(row.Amount) + } + if len(resp) == 0 || !resp[0].BalanceWorkerStarted { //nolint:revive // Wrong. + standard = coin.NewAmountUint64(registrationICEFlakeBonusAmount) + total = standard + totalNoPreStakingBonus = total + } + + return &BalanceSummary{ + Balances: Balances[coin.ICE]{ + Total: total.UnsafeICE(), + TotalNoPreStakingBonus: totalNoPreStakingBonus.UnsafeICE(), + Standard: standard.UnsafeICE(), + PreStaking: preStaking.UnsafeICE(), + T1: t1.UnsafeICE(), + T2: t2.UnsafeICE(), + TotalReferrals: t1.Add(t2).UnsafeICE(), + }, + }, nil +} + +func (r *repository) GetBalanceHistory( //nolint:funlen,gocognit,revive,gocyclo,cyclop,revive // Better to be grouped together. + ctx context.Context, userID string, start, end *time.Time, utcOffset stdlibtime.Duration, limit, offset uint64, +) ([]*BalanceHistoryEntry, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + var factor stdlibtime.Duration + if start.After(*end.Time) { + factor = -1 + } else { + factor = 1 + } + const ( + hoursInADay = 24 + ) + mappedLimit := (limit / hoursInADay) * uint64(r.cfg.GlobalAggregationInterval.Parent/r.cfg.GlobalAggregationInterval.Child) + mappedOffset := (offset / hoursInADay) * uint64(r.cfg.GlobalAggregationInterval.Parent/r.cfg.GlobalAggregationInterval.Child) + typeDetails := make([]string, 0, mappedLimit*2) //nolint:gomnd // Cuz we account for tz diff. + params := make(map[string]any, 1+cap(typeDetails)) + params["user_id"] = userID + for ix := stdlibtime.Duration(0); ix < stdlibtime.Duration(cap(typeDetails)); ix++ { + date := start.Add((ix + stdlibtime.Duration(mappedOffset-mappedLimit)) * factor * r.cfg.GlobalAggregationInterval.Child) + params[fmt.Sprintf("type_detail_child_%v", ix)] = fmt.Sprintf("/%v", date.Format(r.cfg.globalAggregationIntervalChildDateFormat())) + typeDetails = append(typeDetails, fmt.Sprintf(":type_detail_child_%v", ix)) + } + sql := fmt.Sprintf(`SELECT * + FROM balances_%[1]v + WHERE user_id = :user_id + AND (negative = TRUE OR negative = FALSE) + AND type = %[2]v + AND type_detail in (%[3]v)`, r.workerIndex(ctx), totalNoPreStakingBonusBalanceType, strings.Join(typeDetails, ",")) + res := make([]*balance, 0, 2*len(typeDetails)) //nolint:gomnd // Cuz there's a positive and a negative one. + if err := r.db.PrepareExecuteTyped(sql, params, &res); err != nil { + return nil, errors.Wrapf(err, "failed to select balance history for params:%#v", params) + } + if len(res) == 0 { + return make([]*BalanceHistoryEntry, 0, 0), nil //nolint:gosimple // Nope. + } + + adoptions, gErr := getAllAdoptions[coin.ICEFlake](ctx, r.db) + if gErr != nil { + return nil, errors.Wrap(gErr, "failed to getAllAdoptions") + } + + preStakingSummaries, gErr := r.getAllPreStakingSummaries(ctx, userID) + if gErr != nil { + return nil, errors.Wrapf(gErr, "failed to getAllPreStakingSummaries for userID:%v", userID) + } + filteredChildrenByParents := make(map[string]map[string]any, 1+1) + childDateLayout, parentDateLayout := r.cfg.globalAggregationIntervalChildDateFormat(), r.cfg.globalAggregationIntervalParentDateFormat() + for ix := stdlibtime.Duration(mappedOffset); ix < stdlibtime.Duration(mappedLimit+mappedOffset); ix++ { + date := start.Add((ix) * factor * r.cfg.GlobalAggregationInterval.Child) + if factor == -1 && date.Before(*end.Time) { + continue + } + if factor == 1 && date.After(*end.Time) { + continue + } + date = date.Add(utcOffset) + childDateFormat, parentDateFormat := date.Format(childDateLayout), date.Format(parentDateLayout) + if _, found := filteredChildrenByParents[parentDateFormat]; !found { + filteredChildrenByParents[parentDateFormat] = make(map[string]any, mappedLimit) + } + if _, found := filteredChildrenByParents[parentDateFormat][childDateFormat]; !found { + filteredChildrenByParents[parentDateFormat][childDateFormat] = struct{}{} + } + } + resp := make([]*BalanceHistoryEntry, 0, 1+1) + for _, parent := range r.processBalanceHistory(res, factor > 0, utcOffset, adoptions, preStakingSummaries) { + parentDateFormat := parent.Time.Format(parentDateLayout) + if _, found := filteredChildrenByParents[parentDateFormat]; !found { + continue + } + children := make([]*BalanceHistoryEntry, 0, len(parent.TimeSeries)) + for _, child := range parent.TimeSeries { + if _, found := filteredChildrenByParents[parentDateFormat][child.Time.Format(childDateLayout)]; !found { + continue + } + children = append(children, child) + } + if len(children) != 0 { + parent.TimeSeries = children + resp = append(resp, parent) + } + } + + return resp, nil +} + +func (r *repository) processBalanceHistory( //nolint:funlen,gocognit,revive // . + res []*balance, + startDateIsBeforeEndDate bool, + utcOffset stdlibtime.Duration, + adoptions []*Adoption[coin.ICEFlake], + preStakingSummaries []*PreStakingSummary, +) []*BalanceHistoryEntry { + childDateLayout := r.cfg.globalAggregationIntervalChildDateFormat() + parentDateLayout := r.cfg.globalAggregationIntervalParentDateFormat() + parents := make(map[string]*struct { + *BalanceHistoryEntry + children map[string]*BalanceHistoryEntry + }, 1+1) + location := stdlibtime.FixedZone(utcOffset.String(), int(utcOffset.Seconds())) + for _, bal := range res { + child, err := stdlibtime.Parse(childDateLayout, strings.Replace(bal.TypeDetail, "/", "", 1)) + log.Panic(err) //nolint:revive // Intended. + child = child.In(location) + childFormat, parentFormat := child.Format(childDateLayout), child.Format(parentDateLayout) + if _, found := parents[parentFormat]; !found { + parent, pErr := stdlibtime.Parse(parentDateLayout, parentFormat) + log.Panic(pErr) //nolint:revive // Intended. + parents[parentFormat] = &struct { + *BalanceHistoryEntry + children map[string]*BalanceHistoryEntry + }{ + BalanceHistoryEntry: &BalanceHistoryEntry{ + Time: parent.In(location), + Balance: &BalanceHistoryBalanceDiff{amount: coin.ZeroICEFlakes()}, + }, + children: make(map[string]*BalanceHistoryEntry, int(r.cfg.GlobalAggregationInterval.Parent/r.cfg.GlobalAggregationInterval.Child)), + } + } + if _, found := parents[parentFormat].children[childFormat]; !found { + parents[parentFormat].children[childFormat] = &BalanceHistoryEntry{ + Time: child, + Balance: &BalanceHistoryBalanceDiff{amount: coin.ZeroICEFlakes()}, + } + } + parents[parentFormat].children[childFormat].reduceBalance(bal.Negative, bal.Amount) + } + history := make([]*BalanceHistoryEntry, 0, len(parents)) + childMin30TzAdjustment, childMin45TzAdjustment := getTimezoneAdjustments(r.cfg.GlobalAggregationInterval.Child, utcOffset) + parentMin30TzAdjustment, parentMin45TzAdjustment := getTimezoneAdjustments(r.cfg.GlobalAggregationInterval.Parent, utcOffset) + for _, parentVal := range parents { + parentVal.Time = parentVal.Time.Add(parentMin30TzAdjustment).Add(parentMin45TzAdjustment) + parentVal.BalanceHistoryEntry.TimeSeries = make([]*BalanceHistoryEntry, 0, len(parentVal.children)) + var baseMiningRate *coin.ICEFlake + for _, childVal := range parentVal.children { + childVal.Time = childVal.Time.Add(childMin30TzAdjustment).Add(childMin45TzAdjustment) + childVal.applyPreStaking(r.cfg.GlobalAggregationInterval.Child, utcOffset, preStakingSummaries) + baseMiningRate = baseMiningRate.Add(childVal.calculateBalanceDiffBonus(r.cfg.GlobalAggregationInterval.Child, utcOffset, adoptions)) + parentVal.reduceBalance(childVal.Balance.Negative, childVal.Balance.amount) + if r.cfg.GlobalAggregationInterval.Child == stdlibtime.Hour && childVal.Time.Minute() != 0 { + childVal.Time = childVal.Time.Add(-stdlibtime.Duration(childVal.Time.Minute()) * stdlibtime.Minute) + } + childVal.Balance.Amount = childVal.Balance.amount.UnsafeICE() + parentVal.BalanceHistoryEntry.TimeSeries = append(parentVal.BalanceHistoryEntry.TimeSeries, childVal) + } + parentVal.setBalanceDiffBonus(baseMiningRate.DivideUint64(uint64(len(parentVal.children)))) + parentVal.Balance.Amount = parentVal.Balance.amount.UnsafeICE() + sort.SliceStable(parentVal.BalanceHistoryEntry.TimeSeries, func(i, j int) bool { + if startDateIsBeforeEndDate { + return parentVal.BalanceHistoryEntry.TimeSeries[i].Time.Before(parentVal.BalanceHistoryEntry.TimeSeries[j].Time) + } + + return parentVal.BalanceHistoryEntry.TimeSeries[i].Time.After(parentVal.BalanceHistoryEntry.TimeSeries[j].Time) + }) + history = append(history, parentVal.BalanceHistoryEntry) + } + sort.SliceStable(history, func(i, j int) bool { + if startDateIsBeforeEndDate { + return history[i].Time.Before(history[j].Time) + } + + return history[i].Time.After(history[j].Time) + }) + + return history +} + +func getTimezoneAdjustments(aggregationInterval, utcOffset stdlibtime.Duration) (min30Child, min45Child stdlibtime.Duration) { + const halfHourTZFix = 30 * stdlibtime.Minute + const min45TZFix = 45 * stdlibtime.Minute + const min15TZFix = 15 * stdlibtime.Minute + if aggregationInterval >= stdlibtime.Hour && utcOffset.Abs()%stdlibtime.Hour == halfHourTZFix { + min30Child = -halfHourTZFix + } else if aggregationInterval >= stdlibtime.Hour && utcOffset.Abs()%stdlibtime.Hour == min45TZFix { + if utcOffset < 0 { + min45Child = -min15TZFix + } else { + min45Child = -min45TZFix + } + } + + return +} + +func (e *BalanceHistoryEntry) reduceBalance(negative bool, amount *coin.ICEFlake) { //nolint:revive // Not an issue here. + if negative != e.Balance.Negative { + if amount.GT(e.Balance.amount.Uint) { //nolint:gocritic // Nope. + e.Balance.Negative = negative + e.Balance.amount = amount.Subtract(e.Balance.amount) + } else if amount.LT(e.Balance.amount.Uint) { + e.Balance.amount = e.Balance.amount.Subtract(amount) + } else { + e.Balance.Negative = false + e.Balance.amount = coin.ZeroICEFlakes() + } + } else { + e.Balance.amount = e.Balance.amount.Add(amount) + } +} + +func (e *BalanceHistoryEntry) applyPreStaking( //nolint:funlen // . + delta, utcOffset stdlibtime.Duration, preStakingSummaries []*PreStakingSummary, +) *BalanceHistoryEntry { + if len(preStakingSummaries) == 0 { + return e + } + var ( + resultingAmount = coin.ZeroICEFlakes() + endDate = e.Time.Add(delta) + ) + applyProportionalPreStaking := func(ss *PreStakingSummary, startDate stdlibtime.Time) *coin.ICEFlake { + return e.Balance.amount. + MultiplyUint64(percentage100 - ss.Allocation). + DivideUint64(percentage100). + Add(e.Balance.amount. + MultiplyUint64(ss.Allocation * (ss.Bonus + percentage100)). + DivideUint64(percentage100 * percentage100)). + MultiplyUint64(uint64(float64(endDate.Sub(startDate)) * coin.Denomination / float64(delta))). + DivideUint64(coin.Denomination) + } + for ix := len(preStakingSummaries) - 1; ix >= 0; ix-- { + preStakingCreatedAt := preStakingSummaries[ix].CreatedAt.Add(utcOffset) + if preStakingCreatedAt.Before(e.Time.Add(stdlibtime.Nanosecond)) { + resultingAmount = resultingAmount.Add(applyProportionalPreStaking(preStakingSummaries[ix], e.Time)) + + break + } + if preStakingCreatedAt.Before(endDate) && preStakingCreatedAt.After(e.Time.Add(-stdlibtime.Nanosecond)) { + resultingAmount = resultingAmount.Add(applyProportionalPreStaking(preStakingSummaries[ix], preStakingCreatedAt)) + endDate = preStakingCreatedAt + if ix == 0 { + resultingAmount = resultingAmount.Add(e.Balance.amount. + MultiplyUint64(uint64(float64(endDate.Sub(e.Time)) * coin.Denomination / float64(delta))). + DivideUint64(coin.Denomination)) + } + } + } + if !resultingAmount.IsZero() { + e.Balance.amount = resultingAmount + } + + return e +} + +func (e *BalanceHistoryEntry) calculateBalanceDiffBonus( //nolint:funlen // . + delta, utcOffset stdlibtime.Duration, adoptions []*Adoption[coin.ICEFlake], +) (baseMiningRate *coin.ICEFlake) { + endDate := e.Time.Add(delta) + calculateProportionalBaseMiningRate := func(currentBaseMiningRate *coin.ICEFlake, startDate stdlibtime.Time) *coin.ICEFlake { + return currentBaseMiningRate. + MultiplyUint64(uint64(float64(endDate.Sub(startDate)) * coin.Denomination / float64(delta))). + DivideUint64(coin.Denomination) + } + + for ix := len(adoptions) - 1; ix >= 0; ix-- { + if adoptions[ix].AchievedAt == nil { + continue + } + achievedAt := adoptions[ix].AchievedAt.Add(utcOffset) + currentBaseMiningRate := adoptions[ix].BaseMiningRate + if achievedAt.Before(e.Time.Add(stdlibtime.Nanosecond)) { + if baseMiningRate.IsZero() { + baseMiningRate = currentBaseMiningRate + } else { + baseMiningRate = baseMiningRate.Add(calculateProportionalBaseMiningRate(currentBaseMiningRate, e.Time)) + } + + break + } + if achievedAt.Before(endDate) && achievedAt.After(e.Time.Add(-stdlibtime.Nanosecond)) { + baseMiningRate = baseMiningRate.Add(calculateProportionalBaseMiningRate(currentBaseMiningRate, achievedAt)) + endDate = achievedAt + } + } + e.setBalanceDiffBonus(baseMiningRate) + + return baseMiningRate +} + +func (e *BalanceHistoryEntry) setBalanceDiffBonus(baseMiningRate *coin.ICEFlake) { + if e.Balance.Negative { //nolint:gocritic // Wrong. + e.Balance.Bonus = -1 * int64(baseMiningRate. + Add(e.Balance.amount). + MultiplyUint64(percentage100). + Divide(baseMiningRate).Uint64()) + } else if e.Balance.amount.LTE(baseMiningRate.Uint) { + e.Balance.Bonus = -1 * int64(baseMiningRate. + Subtract(e.Balance.amount). + MultiplyUint64(percentage100). + Divide(baseMiningRate).Uint64()) + } else { + e.Balance.Bonus = int64(e.Balance.amount. + Subtract(baseMiningRate). + MultiplyUint64(percentage100). + Divide(baseMiningRate).Uint64()) + } +} + +func (r *repository) insertOrReplaceBalances( //nolint:revive // Alot of SQL params and error handling. Control coupling is ok here. + ctx context.Context, workerIndex uint64, insert bool, updatedAt *time.Time, balances ...*balance, +) error { + if ctx.Err() != nil || len(balances) == 0 { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + const balanceFields = 5 + params := make(map[string]any, 1+(balanceFields*len(balances))) + params["updated_at"] = updatedAt + values := make([]string, 0, len(balances)) + for ix, bal := range balances { + params[fmt.Sprintf("user_id_%v", ix)] = bal.UserID + params[fmt.Sprintf("type_%v", ix)] = bal.Type + params[fmt.Sprintf("type_detail_%v", ix)] = bal.TypeDetail + params[fmt.Sprintf("negative_%v", ix)] = bal.Negative + params[fmt.Sprintf("amount_%v", ix)] = bal.Amount + value := fmt.Sprintf(`(:updated_at,:user_id_%[1]v,:type_%[1]v,:type_detail_%[1]v,:negative_%[1]v,:amount_%[1]v)`, ix) + values = append(values, value) + } + insertOrReplace := "REPLACE" + if insert { + insertOrReplace = "INSERT" + } + sql := fmt.Sprintf(`%v INTO balances_%v (updated_at,user_id,type,type_detail,negative,amount) + VALUES %v`, insertOrReplace, workerIndex, strings.Join(values, ",")) + if _, err := storage.CheckSQLDMLResponse(r.db.PrepareExecute(sql, params)); err != nil { + return errors.Wrapf(err, "failed at %v to %v balances:%#v", updatedAt, insertOrReplace, balances) + } + + return nil +} + +func (r *repository) deleteBalances(ctx context.Context, workerIndex uint64, balances ...*balance) error { + if ctx.Err() != nil || len(balances) == 0 { + return errors.Wrap(ctx.Err(), "context failed") + } + const fields = 4 + params := make(map[string]any, fields*len(balances)) + values := make([]string, 0, len(balances)) + for ix, bal := range balances { + params[fmt.Sprintf("user_id_%v", ix)] = bal.UserID + params[fmt.Sprintf("type_%v", ix)] = bal.Type + params[fmt.Sprintf("type_detail_%v", ix)] = bal.TypeDetail + params[fmt.Sprintf("negative_%v", ix)] = bal.Negative + values = append(values, fmt.Sprintf(`(user_id = :user_id_%[1]v AND negative = :negative_%[1]v AND type = :type_%[1]v AND type_detail = :type_detail_%[1]v)`, ix)) //nolint:lll // . + } + sql := fmt.Sprintf(`DELETE FROM balances_%v WHERE %v`, workerIndex, strings.Join(values, " OR ")) + if _, err := storage.CheckSQLDMLResponse(r.db.PrepareExecute(sql, params)); err != nil { + return errors.Wrapf(err, "failed to DELETE from balances for params:%#v", params) + } + + return nil +} + +func (r *repository) sendAddBalanceCommandMessage(ctx context.Context, cmd *AddBalanceCommand) error { + valueBytes, err := json.MarshalContext(ctx, cmd) + if err != nil { + return errors.Wrapf(err, "failed to marshal %#v", cmd) + } + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: cmd.UserID, + Topic: r.cfg.MessageBroker.Topics[5].Name, + Value: valueBytes, + } + responder := make(chan error, 1) + defer close(responder) + r.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send `%v` message to broker", msg.Topic) +} + +func (s *addBalanceCommandsSource) Process(ctx context.Context, message *messagebroker.Message) error { //nolint:funlen // . + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline while processing message") + } + if len(message.Value) == 0 { + return nil + } + var val AddBalanceCommand + if err := json.UnmarshalContext(ctx, message.Value, &val); err != nil { + return errors.Wrapf(err, "process: cannot unmarshall %v into %#v", string(message.Value), &val) + } + if val.UserID == "" { + return nil + } + bal, err := s.balance(ctx, &val) + if err != nil { + return errors.Wrapf(err, "failed to build balance from %#v", val) + } + type processedAddBalanceCommand struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + UserID string + Key string + } + tuple := &processedAddBalanceCommand{UserID: val.UserID, Key: val.EventID} + if err = storage.CheckNoSQLDMLErr(s.db.InsertTyped("PROCESSED_ADD_BALANCE_COMMANDS", tuple, &[]*processedAddBalanceCommand{})); err != nil { + return errors.Wrapf(err, "failed to insert PROCESSED_ADD_BALANCE_COMMAND:%#v)", tuple) + } + workerIndex, err := s.getWorkerIndex(ctx, val.UserID) + if err != nil { + return errors.Wrapf(err, "failed to getWorkerIndex for userID:%v", val.UserID) + } + err = errors.Wrapf(retry(ctx, func() error { + if err = s.insertOrReplaceBalances(ctx, workerIndex, true, time.New(message.Timestamp), bal); err != nil { + if errors.Is(err, storage.ErrRelationNotFound) { + return err + } + + return errors.Wrapf(backoff.Permanent(err), "failed to insertBalance:%#v", bal) + } + + return nil + }), "permanently failed to insertBalance:%#v", bal) + if err != nil { + err = errors.Wrapf(storage.CheckNoSQLDMLErr(s.db.DeleteTyped("PROCESSED_ADD_BALANCE_COMMANDS", "pk_unnamed_PROCESSED_ADD_BALANCE_COMMANDS_1", []any{val.UserID, val.EventID}, &[]*processedAddBalanceCommand{})), "failed to delete PROCESSED_ADD_BALANCE_COMMAND(%v,%v)", val.UserID, val.EventID) //nolint:lll // . + } + + return err +} + +func (s *addBalanceCommandsSource) balance(ctx context.Context, cmd *AddBalanceCommand) (*balance, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + bal := &balance{ + UserID: cmd.UserID, + Type: pendingXBalanceType, + } + if cmd.Negative != nil && *cmd.Negative { + bal.Negative = *cmd.Negative + } + if !cmd.T1.IsNil() { + bal.Amount = cmd.T1 + bal.TypeDetail = t1BalanceTypeDetail + } + if !cmd.T2.IsNil() { + bal.Amount = cmd.T2 + bal.TypeDetail = t2BalanceTypeDetail + } + if !cmd.Total.IsNil() { + bal.Amount = cmd.Total + } + if !cmd.BaseFactor.IsNil() { + adoption, err := s.getCurrentAdoption(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to getCurrentAdoption") + } + bal.Amount = adoption.BaseMiningRate.Multiply(cmd.BaseFactor) + } + + return bal, nil +} + +func (b *balance) add(amount *coin.ICEFlake) { + b.Amount = b.Amount.Add(amount) +} + +func (b *balance) subtract(amount *coin.ICEFlake) { + b.Amount = b.Amount.Subtract(amount) +} diff --git a/tokenomics/balance_recalculation.go b/tokenomics/balance_recalculation.go new file mode 100644 index 0000000..13e37d5 --- /dev/null +++ b/tokenomics/balance_recalculation.go @@ -0,0 +1,585 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + "sort" + "strings" + stdlibtime "time" + + "github.com/cenkalti/backoff/v4" + "github.com/goccy/go-json" + "github.com/google/uuid" + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/wintr/coin" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) initializeBalanceRecalculationWorker(ctx context.Context, usr *users.User) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + workerIndex := usr.HashCode % r.cfg.WorkerCount + err := retry(ctx, func() error { + if err := r.initializeWorker(ctx, "balance_recalculation_worker_", usr.ID, workerIndex); err != nil { + if errors.Is(err, storage.ErrRelationNotFound) { + return err + } + + return errors.Wrapf(backoff.Permanent(err), "failed to initializeBalanceRecalculationWorker for userID:%v,workerIndex:%v", usr.ID, workerIndex) + } + + return nil + }) + + return errors.Wrapf(err, "permanently failed to initializeBalanceRecalculationWorker for userID:%v,workerIndex:%v", usr.ID, workerIndex) +} + +func (p *processor) startBalanceRecalculationTriggerSeedingStream(ctx context.Context) { + nilBodyForEachWorker := make([]any, p.cfg.WorkerCount) //nolint:makezero // Intended. + ticker := stdlibtime.NewTicker(balanceCalculationProcessingSeedingStreamEmitFrequency) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + log.Error(errors.Wrap(sendMessagesConcurrently[any](ctx, p.sendBalanceRecalculationTriggerMessage, nilBodyForEachWorker), + "failed to sendMessagesConcurrently[sendBalanceRecalculationTriggerMessage]")) + case <-ctx.Done(): + return + } + } +} + +func (p *processor) sendBalanceRecalculationTriggerMessage(ctx context.Context, _ any) error { + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: uuid.NewString(), + Topic: p.cfg.MessageBroker.Topics[9].Name, + Value: nil, + } + responder := make(chan error, 1) + defer close(responder) + p.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send `%v` message to broker", msg.Topic) +} + +func (s *balanceRecalculationTriggerStreamSource) Process(ignoredCtx context.Context, msg *messagebroker.Message) (err error) { + if ignoredCtx.Err() != nil { + return errors.Wrap(ignoredCtx.Err(), "unexpected deadline while processing message") + } + const deadline = 5 * stdlibtime.Minute + ctx, cancel := context.WithTimeout(context.Background(), deadline) + defer cancel() + var ( + now = time.Now() + workerIndex = uint64(msg.Partition) + ) + batch, err := s.getLatestBalancesNewBatch(ctx, now, workerIndex) //nolint:contextcheck // Intended. + if err != nil || len(batch) == 0 { + return errors.Wrapf(err, "failed to getLatestBalancesNewBatch for workerIndex:%v,time:%v", workerIndex, now) + } + if err = s.updateBalances(ctx, now, workerIndex, batch); err != nil { //nolint:contextcheck // Intended. + return errors.Wrapf(err, "failed to updateBalances for workerIndex:%v,time:%v,batch:%#v", workerIndex, now, batch) + } + + return nil +} + +type ( + BalanceRecalculationDetails struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + LastNaturalMiningStartedAt, LastMiningStartedAt, T0LastMiningStartedAt, TMinus1LastMiningStartedAt, + LastMiningEndedAt, T0LastMiningEndedAt, TMinus1LastMiningEndedAt, + PreviousMiningEndedAt, T0PreviousMiningEndedAt, TMinus1PreviousMiningEndedAt, + RollbackUsedAt, T0RollbackUsedAt, TMinus1RollbackUsedAt *time.Time + BaseMiningRate *coin.ICEFlake + UUserID, T0UserID, TMinus1UserID string + T0, T1, T2, ExtraBonus uint64 + } + B = balance + balanceRecalculationRow struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + *B + *BalanceRecalculationDetails + } +) + +func (s *balanceRecalculationTriggerStreamSource) getLatestBalancesNewBatch( //nolint:funlen // Big SQL. + ctx context.Context, now *time.Time, workerIndex uint64, +) ([]*balanceRecalculationRow, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline while processing message") + } + sql := fmt.Sprintf(` +SELECT b.*, + u.last_natural_mining_started_at, + u.last_mining_started_at, + t0.last_mining_started_at AS t0_last_mining_started_at, + tminus1.last_mining_started_at AS tminus1_last_mining_started_at, + u.last_mining_ended_at, + t0.last_mining_ended_at AS t0_last_mining_ended_at, + tminus1.last_mining_ended_at AS tminus1_last_mining_ended_at, + u.previous_mining_ended_at, + t0.previous_mining_ended_at AS t0_previous_mining_ended_at, + tminus1.previous_mining_ended_at AS tminus1_previous_mining_ended_at, + u.rollback_used_at, + t0.rollback_used_at AS t0_rollback_used_at, + tminus1.rollback_used_at AS tminus1_rollback_used_at, + current_adoption.base_mining_rate, + u.user_id AS uuser_id, + t0.user_id AS t0_user_id, + tminus1.user_id AS tminus1_user_id, + (CASE + WHEN 1 = 1 + AND t0.last_mining_ended_at IS NOT NULL + AND t0.last_mining_ended_at > :now_nanos + THEN 1 + ELSE 0 + END) AS t0, + x.t1, + x.t2, + (CASE WHEN IFNULL(eb_worker.extra_bonus_ended_at, 0) > :now_nanos THEN eb_worker.extra_bonus ELSE 0 END) AS extra_bonus +FROM (SELECT COUNT(t1.user_id) AS t1, + x.t2 AS t2, + x.user_id + FROM (SELECT COUNT(t2.user_id) AS t2, + x.user_id + FROM ( SELECT user_id + FROM balance_recalculation_worker_%[2]v + WHERE enabled = TRUE + ORDER BY last_iteration_finished_at + LIMIT %[1]v ) x + LEFT JOIN users t1_mining_not_required + ON t1_mining_not_required.referred_by = x.user_id + AND t1_mining_not_required.user_id != x.user_id + LEFT JOIN users t2 + ON t2.referred_by = t1_mining_not_required.user_id + AND t2.user_id != t1_mining_not_required.user_id + AND t2.user_id != x.user_id + AND t2.last_mining_ended_at IS NOT NULL + AND t2.last_mining_ended_at > :now_nanos + GROUP BY x.user_id + ) x + LEFT JOIN users t1 + ON t1.referred_by = x.user_id + AND t1.user_id != x.user_id + AND t1.last_mining_ended_at IS NOT NULL + AND t1.last_mining_ended_at > :now_nanos + GROUP BY x.user_id + ) x + JOIN (%[3]v) current_adoption + JOIN users u + ON u.user_id = x.user_id + JOIN extra_bonus_processing_worker_%[2]v eb_worker + ON eb_worker.user_id = x.user_id + LEFT JOIN users t0 + ON t0.user_id = u.referred_by + AND t0.user_id != x.user_id + LEFT JOIN users tminus1 + ON tminus1.user_id = t0.referred_by + AND tminus1.user_id != x.user_id + LEFT JOIN balances_%[2]v b + ON b.user_id = u.user_id + AND POSITION('@',b.type_detail) == 0 + AND (CASE + WHEN POSITION('/',b.type_detail) == 1 AND POSITION('&',b.type_detail) == 0 + THEN b.type_detail == :thisDurationTypeDetail OR b.type_detail == :previousDurationTypeDetail OR b.type_detail == :nextDurationTypeDetail + ELSE 1 == 1 + END)`, balanceRecalculationBatchSize, workerIndex, currentAdoptionSQL()) + params := make(map[string]any, 1+1+1+1) + params["now_nanos"] = now + params["nextDurationTypeDetail"] = fmt.Sprintf("/%v", now.Add(s.cfg.GlobalAggregationInterval.Child).Format(s.cfg.globalAggregationIntervalChildDateFormat())) //nolint:lll // . + params["thisDurationTypeDetail"] = fmt.Sprintf("/%v", now.Format(s.cfg.globalAggregationIntervalChildDateFormat())) + params["previousDurationTypeDetail"] = fmt.Sprintf("/%v", now.Add(-1*s.cfg.GlobalAggregationInterval.Child).Format(s.cfg.globalAggregationIntervalChildDateFormat())) //nolint:lll // . + const estimatedBalancesPerUser = 14 + resp := make([]*balanceRecalculationRow, 0, balanceRecalculationBatchSize*estimatedBalancesPerUser) + if err := s.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return nil, errors.Wrapf(err, "failed to select new balance recalculation batch for workerIndex:%v,params:%#v", workerIndex, params) + } + + return resp, nil +} + +func (s *balanceRecalculationTriggerStreamSource) updateBalances( + ctx context.Context, now *time.Time, workerIndex uint64, batch []*balanceRecalculationRow, +) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "context failed") + } + balancesForReplace, balancesForDelete, processingStoppedForUserIDs, dayOffStartedEvents, userIDs := s.recalculateBalances(now, batch) + if err := sendMessagesConcurrently(ctx, s.sendFreeMiningSessionStartedMessage, dayOffStartedEvents); err != nil { + return errors.Wrapf(err, "failed to sendMessagesConcurrently[sendFreeMiningSessionStartedMessage] for dayOffStartedEvents:%#v", dayOffStartedEvents) + } + if err := s.insertOrReplaceBalances(ctx, workerIndex, false, now, balancesForReplace...); err != nil { + return errors.Wrapf(err, "failed to replaceBalances: %#v", balancesForReplace) + } + if err := s.deleteBalances(ctx, workerIndex, balancesForDelete...); err != nil { + return errors.Wrapf(err, "failed to deleteBalances: %#v", balancesForDelete) + } + if err := s.updateLastIterationFinishedAt(ctx, workerIndex, userIDs); err != nil { + return errors.Wrapf(err, "failed to updateLastIterationFinishedAt, workerIndex:%v,userIDs:%#v", workerIndex, userIDs) + } + if err := s.stopWorkerForUsers(ctx, workerIndex, processingStoppedForUserIDs); err != nil { + return errors.Wrapf(err, "failed to stopWorkerForUsers, workerIndex:%v,userIDs:%#v", workerIndex, processingStoppedForUserIDs) + } + + return nil +} + +//nolint:funlen,gocognit,gocritic,gocyclo,revive,cyclop // . +func (s *balanceRecalculationTriggerStreamSource) recalculateBalances( + now *time.Time, rows []*balanceRecalculationRow, +) (balancesForReplace, balancesForDelete []*balance, processingStoppedForUserIDs map[string]*time.Time, dayOffStartedEvents []*FreeMiningSessionStarted, userIDs []string) { //nolint:lll // . + balancesForReplace = make([]*balance, 0, len(rows)) + balancesForDelete = make([]*balance, 0, 0) //nolint:gosimple // Nope. + processingStoppedForUserIDs = make(map[string]*time.Time) + dayOffStartedEvents = make([]*FreeMiningSessionStarted, 0, 0) //nolint:gosimple // Nope. + userIDs = make([]string, 0, len(rows)) + var ( + thisDurationTypeDetail = fmt.Sprintf("/%v", now.Format(s.cfg.globalAggregationIntervalChildDateFormat())) + untilThisDurationTypeDetail = fmt.Sprintf("@%v", now.Format(s.cfg.globalAggregationIntervalChildDateFormat())) + balancesPerUser = make(map[string]map[string]*balance, balanceRecalculationBatchSize) + aggregatedPendingTotalBalancesPerUser = make(map[string]map[bool]*balance, balanceRecalculationBatchSize) + aggregatedPendingT1BalancesPerUser = make(map[string]map[bool]*balance, balanceRecalculationBatchSize) + aggregatedPendingT2BalancesPerUser = make(map[string]map[bool]*balance, balanceRecalculationBatchSize) + balanceRecalculationDetailsPerUser = make(map[string]*BalanceRecalculationDetails, balanceRecalculationBatchSize) + ) + for _, row := range rows { + userID := row.BalanceRecalculationDetails.UUserID + if _, found := balanceRecalculationDetailsPerUser[userID]; !found { + balanceRecalculationDetailsPerUser[userID] = row.BalanceRecalculationDetails + } + if _, found := balancesPerUser[userID]; !found { + balancesPerUser[userID] = make(map[string]*balance) + userIDs = append(userIDs, userID) + } + if row.B == nil || row.B.UserID == "" { + continue + } + if row.B.Type == pendingXBalanceType { //nolint:nestif // It's fine. + switch { + case strings.HasPrefix(row.B.TypeDetail, t1BalanceTypeDetail): + if _, found := aggregatedPendingT1BalancesPerUser[userID]; !found { + aggregatedPendingT1BalancesPerUser[userID] = make(map[bool]*balance, 1+1) + } + if existing, found := aggregatedPendingT1BalancesPerUser[userID][row.B.Negative]; !found { + aggregatedPendingT1BalancesPerUser[userID][row.B.Negative] = row.B + } else { + existing.add(row.B.Amount) + } + case strings.HasPrefix(row.B.TypeDetail, t2BalanceTypeDetail): + if _, found := aggregatedPendingT2BalancesPerUser[userID]; !found { + aggregatedPendingT2BalancesPerUser[userID] = make(map[bool]*balance, 1+1) + } + if existing, found := aggregatedPendingT2BalancesPerUser[userID][row.B.Negative]; !found { + aggregatedPendingT2BalancesPerUser[userID][row.B.Negative] = row.B + } else { + existing.add(row.B.Amount) + } + case row.B.TypeDetail == "": + if _, found := aggregatedPendingTotalBalancesPerUser[userID]; !found { + aggregatedPendingTotalBalancesPerUser[userID] = make(map[bool]*balance, 1+1) + } + if existing, found := aggregatedPendingTotalBalancesPerUser[userID][row.Negative]; !found { + aggregatedPendingTotalBalancesPerUser[userID][row.B.Negative] = row.B + } else { + existing.add(row.B.Amount) + } + default: + log.Panic(fmt.Sprintf("unknown typeDetail `%v`", row.B.TypeDetail)) + } + clone := *row.B + clone.UpdatedAt = now + clone.Amount = coin.ZeroICEFlakes() + balancesForReplace = append(balancesForReplace, &clone) + balancesForDelete = append(balancesForDelete, &clone) + } else { + balancesPerUser[userID][fmt.Sprint(row.B.Negative, row.B.Type, row.B.TypeDetail)] = row.B + } + } + for userID, balancesByPK := range balancesPerUser { + var ( + details = balanceRecalculationDetailsPerUser[userID] + aggregatedPendingTotalBalances = aggregatedPendingTotalBalancesPerUser[userID] + aggregatedPendingT1Balances = aggregatedPendingT1BalancesPerUser[userID] + aggregatedPendingT2Balances = aggregatedPendingT2BalancesPerUser[userID] + previousDurationTypeDetail, previousElapsedDuration, nowElapsedDuration = s.calculateElapsedDurations(balancesByPK, details, now) + previousT0ElapsedDuration, nowT0ElapsedDuration = s.calculateElapsedT0ReverseDurations(balancesByPK, details, now) + previousTMinus1ElapsedDuration, nowTMinus1ElapsedDuration = s.calculateElapsedTMinus1ReverseDurations(balancesByPK, details, now) + ) + if previousDurationTypeDetail == "" { + previousDurationTypeDetail = thisDurationTypeDetail + } + if dayOffStarted := s.didANewFreeMiningSessionJustStart(balancesByPK, details, now); dayOffStarted != nil { + dayOffStartedEvents = append(dayOffStartedEvents, dayOffStarted) + } + + s.processDegradationForTotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.processDegradationForT0TotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.processDegradationForT1TotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.processDegradationForT2TotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.processDegradationForT0ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.processDegradationForTMinus1ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + + s.processPreviousIncompleteTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, previousElapsedDuration) + s.processPreviousIncompleteTMinus1ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, previousTMinus1ElapsedDuration) + s.processPreviousIncompleteT0ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, previousT0ElapsedDuration) + s.processPreviousIncompleteT0TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, previousElapsedDuration) + s.processPreviousIncompleteT1TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, previousElapsedDuration) + s.processPreviousIncompleteT2TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, previousElapsedDuration) + s.processPreviousIncompleteThisDurationTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, previousElapsedDuration, previousDurationTypeDetail) + + s.rollbackTotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.rollbackTMinus1ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.rollbackT0ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.rollbackT0TotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.rollbackT1TotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + s.rollbackT2TotalNoPreStakingBonusBalanceType(balancesByPK, details, now) + + s.processTotalNoPreStakingBonusBalanceType(balancesByPK, aggregatedPendingTotalBalances, details, now, nowElapsedDuration) + s.processTMinus1ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, nowTMinus1ElapsedDuration) + s.processT0ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, nowT0ElapsedDuration) + s.processT0TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, nowElapsedDuration) + s.processT1TotalNoPreStakingBonusBalanceType(balancesByPK, aggregatedPendingT1Balances, details, now, nowElapsedDuration) + s.processT2TotalNoPreStakingBonusBalanceType(balancesByPK, aggregatedPendingT2Balances, details, now, nowElapsedDuration) + s.processThisDurationTotalNoPreStakingBonusBalanceType(balancesByPK, aggregatedPendingTotalBalances, details, now, nowElapsedDuration, thisDurationTypeDetail) + + s.processTotalNoPreStakingBonusUntilThisDurationBalanceType(balancesByPK, details, untilThisDurationTypeDetail, userID) + + zeroBalancesRequiredToStop := make(map[string]*coin.ICEFlake, 3) //nolint:gomnd // There's only 3, untilThisDuration, revT0, revT-1 + for balPK, bal := range balancesByPK { + if bal == nil || bal.Amount.IsNil() { + delete(balancesByPK, balPK) + + continue + } + bal.UpdatedAt = now + if bal.Type == totalNoPreStakingBonusBalanceType && + (details.t0Changed(bal.TypeDetail) || details.reverseT0Changed(bal.TypeDetail) || details.reverseTMinus1Changed(bal.TypeDetail)) { + bal.Amount = coin.ZeroICEFlakes() + } + if bal.Type == totalNoPreStakingBonusBalanceType && + !bal.Negative && + (bal.TypeDetail == untilThisDurationTypeDetail || + bal.TypeDetail == details.reverseT0TypeDetail() || + bal.TypeDetail == details.reverseTMinus1TypeDetail()) { + zeroBalancesRequiredToStop[balPK] = bal.Amount + } + if bal.Amount.IsZero() { + balancesForDelete = append(balancesForDelete, bal) + } + balancesForReplace = append(balancesForReplace, bal) + } + shouldStop := true + for _, bal := range zeroBalancesRequiredToStop { + if !bal.IsZero() { + shouldStop = false + + break + } + } + if shouldStop { + processingStoppedForUserIDs[userID] = details.LastMiningEndedAt + } + } + + return balancesForReplace, balancesForDelete, processingStoppedForUserIDs, dayOffStartedEvents, userIDs +} + +func (s *balanceRecalculationTriggerStreamSource) calculateElapsedDurations( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) (previousDurationTypeDetail string, previousElapsedDuration, nowElapsedDuration stdlibtime.Duration) { + totalBalance := s.getOrInitBalance(false, "", details.UUserID, balancesByPK) + if totalBalance.UpdatedAt == nil { + return "", 0, now.Sub(*details.LastMiningStartedAt.Time) + } + if details.LastMiningEndedAt.Before(*now.Time) && totalBalance.UpdatedAt.Before(*details.LastMiningEndedAt.Time) { + previousDurationTypeDetail = fmt.Sprintf("/%v", details.LastMiningEndedAt.Format(s.cfg.globalAggregationIntervalChildDateFormat())) + previousElapsedDuration = details.LastMiningEndedAt.Sub(*totalBalance.UpdatedAt.Time) + nowElapsedDuration = now.Sub(*details.LastMiningEndedAt.Time) + } + if details.PreviousMiningEndedAt != nil && + details.PreviousMiningEndedAt.Before(*totalBalance.UpdatedAt.Time) && + details.LastMiningEndedAt.After(*now.Time) && + details.LastMiningStartedAt.Before(*now.Time) && + totalBalance.UpdatedAt.Before(*details.LastMiningStartedAt.Time) { + previousDurationTypeDetail = fmt.Sprintf("/%v", details.LastMiningStartedAt.Format(s.cfg.globalAggregationIntervalChildDateFormat())) + previousElapsedDuration = details.LastMiningStartedAt.Sub(*totalBalance.UpdatedAt.Time) + nowElapsedDuration = now.Sub(*details.LastMiningStartedAt.Time) + } + if nowElapsedDuration == 0 { + nowElapsedDuration = now.Sub(*totalBalance.UpdatedAt.Time) + } + + return previousDurationTypeDetail, previousElapsedDuration, nowElapsedDuration +} + +func (s *balanceRecalculationTriggerStreamSource) didANewFreeMiningSessionJustStart( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) *FreeMiningSessionStarted { + if details.LastMiningEndedAt.Before(*now.Time) { + return nil + } + totalBalance := s.getOrInitBalance(false, "", details.UUserID, balancesByPK) + if totalBalance.UpdatedAt == nil { + return nil + } + ms := s.calculateMiningSession(now, details.LastNaturalMiningStartedAt, details.LastMiningEndedAt) + if ms == nil || ms.Free == nil || !*ms.Free || totalBalance.UpdatedAt.After(*ms.StartedAt.Time) { + return nil + } + + return &FreeMiningSessionStarted{ + StartedAt: ms.StartedAt, + EndedAt: ms.EndedAt, + UserID: details.UUserID, + ID: fmt.Sprint(ms.StartedAt.UnixNano() / s.cfg.MiningSessionDuration.Max.Nanoseconds()), + RemainingFreeMiningSessions: s.calculateRemainingFreeMiningSessions(now, details.LastMiningEndedAt), + MiningStreak: s.calculateMiningStreak(now, details.LastMiningStartedAt, details.LastMiningEndedAt), + } +} + +func (*balanceRecalculationTriggerStreamSource) getOrInitBalance( + negative bool, typeDetail, userID string, balancesByPK map[string]*balance, +) *balance { + if val, found := balancesByPK[fmt.Sprint(negative, totalNoPreStakingBonusBalanceType, typeDetail)]; !found { + val = &balance{ + UserID: userID, + TypeDetail: typeDetail, + Type: totalNoPreStakingBonusBalanceType, + Negative: negative, + } + balancesByPK[fmt.Sprint(negative, totalNoPreStakingBonusBalanceType, typeDetail)] = val + + return val + } else { //nolint:revive // Nope. + return val + } +} + +func (*balanceRecalculationTriggerStreamSource) getBalance( + negative bool, typeDetail string, balancesByPK map[string]*balance, +) *balance { + return balancesByPK[fmt.Sprint(negative, totalNoPreStakingBonusBalanceType, typeDetail)] +} + +const ( + degradationPrecision = 1.005 +) + +//nolint:revive // Not a problem here. +func (r *repository) calculateDegradation( + elapsedDuration stdlibtime.Duration, referenceAmount *coin.ICEFlake, aggressive bool, +) *coin.ICEFlake { + if elapsedDuration < 0 { + return nil + } + + if aggressive { + return referenceAmount. + MultiplyUint64(uint64(float64(elapsedDuration) * degradationPrecision)). + DivideUint64(uint64(r.cfg.RollbackNegativeMining.Available.Until - r.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter)) + } + + return referenceAmount. + MultiplyUint64(uint64(float64(elapsedDuration) * degradationPrecision)). + DivideUint64(uint64(r.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter)) +} + +func (s *balanceRecalculationTriggerStreamSource) processLastXPositiveMiningSessions( //nolint:revive // Not an issue here. + balancesByPK map[string]*balance, shouldTransformNegative bool, dateExtractionSeparator, lastXMiningTypeDetail, userID string, +) { + type datedBalance struct { + b *balance + date *time.Time + } + actualLastXMiningSessionBalances := make([]*datedBalance, 0, 0) //nolint:gosimple // Prefer to be more descriptive. + for _, bal := range balancesByPK { + if parts := strings.Split(bal.TypeDetail, dateExtractionSeparator); len(parts) == 1+1 && parts[0] == "" { //nolint:revive,gocritic // Nope. + date, err := stdlibtime.Parse(s.cfg.lastXMiningSessionsCollectingIntervalDateFormat(), parts[1]) + log.Panic(err) //nolint:revive // Intended. + if shouldTransformNegative && bal.Negative { + bal.Negative = false + } + actualLastXMiningSessionBalances = append(actualLastXMiningSessionBalances, &datedBalance{b: bal, date: time.New(date)}) + } + } + if len(actualLastXMiningSessionBalances) > int(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter/s.cfg.MiningSessionDuration.Max) { + sort.SliceStable(actualLastXMiningSessionBalances, func(i, j int) bool { + return actualLastXMiningSessionBalances[i].date.Before(*actualLastXMiningSessionBalances[j].date.Time) + }) + actualLastXMiningSessionBalances[0].b.Amount = coin.ZeroICEFlakes() + } + totalPositiveLastXMiningSessions := s.getOrInitBalance(false, lastXMiningTypeDetail, userID, balancesByPK) + for _, bal := range actualLastXMiningSessionBalances { + totalPositiveLastXMiningSessions.add(bal.b.Amount) + } +} + +func (s *balanceRecalculationTriggerStreamSource) updateLastIterationFinishedAt( + ctx context.Context, workerIndex uint64, userIDs []string, +) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + const table = "balance_recalculation_worker_" + params := make(map[string]any, 1) + params["last_iteration_finished_at"] = time.Now() + err := s.updateWorkerFields(ctx, workerIndex, table, params, userIDs...) + + return errors.Wrapf(err, "failed to updateWorkerFields for workerIndex:%v,table:%q,params:%#v,userIDs:%#v", workerIndex, table, params, userIDs) +} + +func (s *balanceRecalculationTriggerStreamSource) stopWorkerForUsers( + ctx context.Context, workerIndex uint64, lastMiningEndedAtPerUserID map[string]*time.Time, +) error { + if ctx.Err() != nil || len(lastMiningEndedAtPerUserID) == 0 { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + params := make(map[string]any, (1+1)*len(lastMiningEndedAtPerUserID)) + conditions := make([]string, 0, len(lastMiningEndedAtPerUserID)) + ix := 0 + for userID, lastMiningEndedAt := range lastMiningEndedAtPerUserID { + params[fmt.Sprintf("user_id%v", ix)] = userID + params[fmt.Sprintf("last_mining_ended_at%v", ix)] = lastMiningEndedAt + conditions = append(conditions, fmt.Sprintf("(user_id = :user_id%[1]v AND last_mining_ended_at = :last_mining_ended_at%[1]v)", ix)) + ix++ + } + sql := fmt.Sprintf(`UPDATE balance_recalculation_worker_%[1]v + SET enabled = FALSE + WHERE %v`, workerIndex, strings.Join(conditions, " OR ")) + if _, err := storage.CheckSQLDMLResponse(s.db.PrepareExecute(sql, params)); err != nil { + return errors.Wrapf(err, "failed to update balance_recalculation_worker_%v SET enabled = FALSE for params:%#v", workerIndex, params) + } + + return nil +} + +func (s *balanceRecalculationTriggerStreamSource) sendFreeMiningSessionStartedMessage(ctx context.Context, fmss *FreeMiningSessionStarted) error { + valueBytes, err := json.MarshalContext(ctx, fmss) + if err != nil { + return errors.Wrapf(err, "failed to marshal %#v", fmss) + } + + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: fmss.UserID, + Topic: s.cfg.MessageBroker.Topics[8].Name, + Value: valueBytes, + } + + responder := make(chan error, 1) + defer close(responder) + s.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send %v message to broker, msg:%#v", msg.Topic, fmss) +} diff --git a/tokenomics/balance_recalculation_t-1_reverse_total_no_pre_staking_bonus_amount.go b/tokenomics/balance_recalculation_t-1_reverse_total_no_pre_staking_bonus_amount.go new file mode 100644 index 0000000..437529a --- /dev/null +++ b/tokenomics/balance_recalculation_t-1_reverse_total_no_pre_staking_bonus_amount.go @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "fmt" + "strings" + stdlibtime "time" + + "github.com/ice-blockchain/wintr/coin" + "github.com/ice-blockchain/wintr/time" +) + +//nolint:dupl // Prefer decoupling. +func (s *balanceRecalculationTriggerStreamSource) calculateElapsedTMinus1ReverseDurations( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) (previousElapsedDuration, nowElapsedDuration stdlibtime.Duration) { + if details.TMinus1LastMiningStartedAt == nil { + return 0, 0 + } + totalBalance := s.getBalance(false, details.reverseTMinus1TypeDetail(), balancesByPK) + if totalBalance == nil || totalBalance.UpdatedAt == nil { + return 0, now.Sub(*details.TMinus1LastMiningStartedAt.Time) + } + if details.TMinus1LastMiningEndedAt.Before(*now.Time) && totalBalance.UpdatedAt.Before(*details.TMinus1LastMiningEndedAt.Time) { + previousElapsedDuration = details.TMinus1LastMiningEndedAt.Sub(*totalBalance.UpdatedAt.Time) + nowElapsedDuration = now.Sub(*details.TMinus1LastMiningEndedAt.Time) + } + if details.TMinus1PreviousMiningEndedAt != nil && + details.TMinus1PreviousMiningEndedAt.Before(*totalBalance.UpdatedAt.Time) && + details.TMinus1LastMiningEndedAt.After(*now.Time) && + details.TMinus1LastMiningStartedAt.Before(*now.Time) && + totalBalance.UpdatedAt.Before(*details.TMinus1LastMiningStartedAt.Time) { + previousElapsedDuration = details.TMinus1LastMiningStartedAt.Sub(*totalBalance.UpdatedAt.Time) + nowElapsedDuration = now.Sub(*details.TMinus1LastMiningStartedAt.Time) + } + if nowElapsedDuration == 0 { + nowElapsedDuration = now.Sub(*totalBalance.UpdatedAt.Time) + } + + return previousElapsedDuration, nowElapsedDuration +} + +//nolint:gocognit // Hard to improve. +func (s *balanceRecalculationTriggerStreamSource) processPreviousIncompleteTMinus1ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + if elapsedDuration == 0 || + details.TMinus1UserID == details.UUserID || + details.TMinus1UserID == details.T0UserID || + details.TMinus1UserID == "" || + details.TMinus1LastMiningEndedAt == nil { + return + } + isPositiveMining := details.LastMiningEndedAt.After(*now.Time) + isWithinCurrentTMinus1PositiveMiningSession := details.TMinus1LastMiningStartedAt.Before(*details.LastMiningEndedAt.Time) && + details.TMinus1LastMiningEndedAt.After(*details.LastMiningEndedAt.Time) + wasPreviousTMinus1MiningPositive := isWithinCurrentTMinus1PositiveMiningSession || + (details.TMinus1PreviousMiningEndedAt != nil && details.TMinus1PreviousMiningEndedAt.After(*details.LastMiningEndedAt.Time)) + if wasPreviousTMinus1MiningPositive && !isPositiveMining { + s.mintTMinus1ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration) + } else if !wasPreviousTMinus1MiningPositive { + s.slashTMinus1ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } +} + +func (s *balanceRecalculationTriggerStreamSource) rollbackTMinus1ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + if details.TMinus1UserID == details.UUserID || + details.TMinus1UserID == details.T0UserID || + details.TMinus1UserID == "" || + details.TMinus1LastMiningEndedAt == nil || + details.TMinus1LastMiningEndedAt.Before(*now.Time) { + return + } + negativeBalance := s.getBalance(true, details.reverseTMinus1TypeDetail(), balancesByPK) + if negativeBalance == nil || negativeBalance.Amount.IsZero() { + return + } + if details.TMinus1RollbackUsedAt != nil { + positiveBalance := s.getOrInitBalance(false, details.reverseTMinus1TypeDetail(), details.UUserID, balancesByPK) + positiveBalance.add(negativeBalance.Amount) + } + negativeBalance.Amount = coin.ZeroICEFlakes() +} + +func (s *balanceRecalculationTriggerStreamSource) processTMinus1ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + if details.TMinus1UserID == details.UUserID || + details.TMinus1UserID == details.T0UserID || + details.TMinus1UserID == "" || + details.TMinus1LastMiningEndedAt == nil { + return + } + defer func() { + s.getBalance(false, lastXMiningSessionsReverseTMinus1TypeDetail, balancesByPK).Amount = nil + }() + isPositiveMining := details.LastMiningEndedAt.After(*now.Time) + isRefPositiveMining := details.TMinus1LastMiningEndedAt.After(*now.Time) + if isPositiveMining && isRefPositiveMining { + s.mintTMinus1ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration) + } else if !isRefPositiveMining { + s.slashTMinus1ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } +} + +func (s *balanceRecalculationTriggerStreamSource) mintTMinus1ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + params := &userMiningRateRecalculationParameters{T2: 1} + mintedAmount := s.calculateMintedStandardCoins(details.BaseMiningRate, params, elapsedDuration, true) + positiveBalance := s.getOrInitBalance(false, details.reverseTMinus1TypeDetail(), details.UUserID, balancesByPK) + positiveBalance.add(mintedAmount) + positiveTotalThisMiningSessionBalance := s.getOrInitBalance(false, s.reverseTMinus1ThisDurationDegradationReferenceTypeDetail(details, now), details.UUserID, balancesByPK) //nolint:lll // . + positiveTotalThisMiningSessionBalance.add(mintedAmount) +} + +//nolint:dupl,revive // Prefer decoupling. +func (s *balanceRecalculationTriggerStreamSource) slashTMinus1ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + positiveBalance := s.getOrInitBalance(false, details.reverseTMinus1TypeDetail(), details.UUserID, balancesByPK) + if positiveBalance.Amount.IsZero() { + return + } + aggressive := details.TMinus1LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + var referenceAmount *coin.ICEFlake + if aggressive { + referenceAmount = s.getBalance(false, details.reverseTMinus1AggressiveDegradationReferenceTypeDetail(), balancesByPK).Amount + } else { + referenceAmount = s.getBalance(false, lastXMiningSessionsReverseTMinus1TypeDetail, balancesByPK).Amount + } + slashedAmount := s.calculateDegradation(elapsedDuration, referenceAmount, aggressive) + positiveBalance.subtract(slashedAmount) + if details.TMinus1RollbackUsedAt == nil || (previous && details.TMinus1LastMiningEndedAt.After(*now.Time) && details.TMinus1RollbackUsedAt.Equal(*details.TMinus1LastMiningStartedAt.Time)) { //nolint:lll // . + negativeBalance := s.getOrInitBalance(true, details.reverseTMinus1TypeDetail(), details.UUserID, balancesByPK) + negativeBalance.add(slashedAmount) + } +} + +func (s *balanceRecalculationTriggerStreamSource) processDegradationForTMinus1ReverseTotalNoPreStakingBonusBalanceType( //nolint:gocognit // Barely. + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + if details.TMinus1UserID == details.UUserID || + details.TMinus1UserID == details.T0UserID || + details.TMinus1UserID == "" || + details.TMinus1LastMiningEndedAt == nil { + return + } + isPositiveMining := details.TMinus1LastMiningEndedAt.After(*now.Time) + s.processLastXPositiveMiningSessions(balancesByPK, false, details.reverseTMinus1TypeDetail()+"/&", lastXMiningSessionsReverseTMinus1TypeDetail, details.UUserID) //nolint:lll // . + + aggressive := details.TMinus1LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + referenceBalance := s.getBalance(false, details.reverseTMinus1AggressiveDegradationReferenceTypeDetail(), balancesByPK) + if !isPositiveMining && aggressive && (referenceBalance == nil || referenceBalance.Amount.IsNil()) { + positiveBalance := s.getOrInitBalance(false, details.reverseTMinus1TypeDetail(), details.UUserID, balancesByPK) + referenceBalance = s.getOrInitBalance(false, details.reverseTMinus1AggressiveDegradationReferenceTypeDetail(), details.UUserID, balancesByPK) + referenceBalance.add(positiveBalance.Amount) + } + if isPositiveMining && referenceBalance != nil && !referenceBalance.Amount.IsZero() { + referenceBalance.Amount = coin.ZeroICEFlakes() + } +} + +func (s *balanceRecalculationTriggerStreamSource) reverseTMinus1ThisDurationDegradationReferenceTypeDetail(details *BalanceRecalculationDetails, now *time.Time) string { //nolint:lll // . + return fmt.Sprintf("%v/&%v", details.reverseTMinus1TypeDetail(), s.lastXMiningSessionsCollectingIntervalDateFormat(now)) +} + +const ( + lastXMiningSessionsReverseTMinus1TypeDetail = reverseTMinus1BalanceTypeDetail + "/0" +) + +func (d *BalanceRecalculationDetails) reverseTMinus1TypeDetail() string { + return fmt.Sprintf("%v_%v", reverseTMinus1BalanceTypeDetail, d.TMinus1UserID) +} + +func (d *BalanceRecalculationDetails) reverseTMinus1AggressiveDegradationReferenceTypeDetail() string { + return fmt.Sprintf("%v_", d.reverseTMinus1TypeDetail()) +} + +func (d *BalanceRecalculationDetails) reverseTMinus1Changed(typeDetail string) bool { + if !strings.HasPrefix(typeDetail, reverseTMinus1BalanceTypeDetail+"_") { + return false + } + userID := strings.Replace(typeDetail, reverseTMinus1BalanceTypeDetail+"_", "", 1) + userID = strings.Replace(userID, "_", "", 1) + userID = strings.Split(userID, "/")[0] + + return d.TMinus1UserID != userID +} diff --git a/tokenomics/balance_recalculation_t0_reverse_total_no_pre_staking_bonus_amount.go b/tokenomics/balance_recalculation_t0_reverse_total_no_pre_staking_bonus_amount.go new file mode 100644 index 0000000..72c2dfc --- /dev/null +++ b/tokenomics/balance_recalculation_t0_reverse_total_no_pre_staking_bonus_amount.go @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "fmt" + "strings" + stdlibtime "time" + + "github.com/ice-blockchain/wintr/coin" + "github.com/ice-blockchain/wintr/time" +) + +//nolint:dupl // Prefer decoupling. +func (s *balanceRecalculationTriggerStreamSource) calculateElapsedT0ReverseDurations( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) (previousElapsedDuration, nowElapsedDuration stdlibtime.Duration) { + if details.T0LastMiningStartedAt == nil { + return 0, 0 + } + totalBalance := s.getBalance(false, details.reverseT0TypeDetail(), balancesByPK) + if totalBalance == nil || totalBalance.UpdatedAt == nil { + return 0, now.Sub(*details.T0LastMiningStartedAt.Time) + } + if details.T0LastMiningEndedAt.Before(*now.Time) && totalBalance.UpdatedAt.Before(*details.T0LastMiningEndedAt.Time) { + previousElapsedDuration = details.T0LastMiningEndedAt.Sub(*totalBalance.UpdatedAt.Time) + nowElapsedDuration = now.Sub(*details.T0LastMiningEndedAt.Time) + } + if details.T0PreviousMiningEndedAt != nil && + details.T0PreviousMiningEndedAt.Before(*totalBalance.UpdatedAt.Time) && + details.T0LastMiningEndedAt.After(*now.Time) && + details.T0LastMiningStartedAt.Before(*now.Time) && + totalBalance.UpdatedAt.Before(*details.T0LastMiningStartedAt.Time) { + previousElapsedDuration = details.T0LastMiningStartedAt.Sub(*totalBalance.UpdatedAt.Time) + nowElapsedDuration = now.Sub(*details.T0LastMiningStartedAt.Time) + } + if nowElapsedDuration == 0 { + nowElapsedDuration = now.Sub(*totalBalance.UpdatedAt.Time) + } + + return previousElapsedDuration, nowElapsedDuration +} + +func (s *balanceRecalculationTriggerStreamSource) processPreviousIncompleteT0ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + if elapsedDuration == 0 || + details.T0UserID == details.UUserID || + details.T0UserID == "" || + details.T0LastMiningEndedAt == nil { + return + } + isPositiveMining := details.LastMiningEndedAt.After(*now.Time) + isWithinCurrentT0PositiveMiningSession := details.T0LastMiningStartedAt.Before(*details.LastMiningEndedAt.Time) && + details.T0LastMiningEndedAt.After(*details.LastMiningEndedAt.Time) + wasPreviousT0MiningPositive := isWithinCurrentT0PositiveMiningSession || + (details.T0PreviousMiningEndedAt != nil && details.T0PreviousMiningEndedAt.After(*details.LastMiningEndedAt.Time)) + if wasPreviousT0MiningPositive && !isPositiveMining { + s.mintT0ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration) + } else if !wasPreviousT0MiningPositive { + s.slashT0ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } +} + +func (s *balanceRecalculationTriggerStreamSource) rollbackT0ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + if details.T0UserID == details.UUserID || + details.T0UserID == "" || + details.T0LastMiningEndedAt == nil || + details.T0LastMiningEndedAt.Before(*now.Time) { + return + } + negativeBalance := s.getBalance(true, details.reverseT0TypeDetail(), balancesByPK) + if negativeBalance == nil || negativeBalance.Amount.IsZero() { + return + } + if details.T0RollbackUsedAt != nil { + positiveBalance := s.getOrInitBalance(false, details.reverseT0TypeDetail(), details.UUserID, balancesByPK) + positiveBalance.add(negativeBalance.Amount) + } + negativeBalance.Amount = coin.ZeroICEFlakes() +} + +func (s *balanceRecalculationTriggerStreamSource) processT0ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + if details.T0UserID == details.UUserID || + details.T0UserID == "" || + details.T0LastMiningEndedAt == nil { + return + } + defer func() { + s.getBalance(false, lastXMiningSessionsReverseT0TypeDetail, balancesByPK).Amount = nil + }() + isPositiveMining := details.LastMiningEndedAt.After(*now.Time) + isRefPositiveMining := details.T0LastMiningEndedAt.After(*now.Time) + if isPositiveMining && isRefPositiveMining { + s.mintT0ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration) + } else if !isRefPositiveMining { + s.slashT0ReverseTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } +} + +func (s *balanceRecalculationTriggerStreamSource) mintT0ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + params := &userMiningRateRecalculationParameters{T0: 1} + mintedAmount := s.calculateMintedStandardCoins(details.BaseMiningRate, params, elapsedDuration, true) + positiveBalance := s.getOrInitBalance(false, details.reverseT0TypeDetail(), details.UUserID, balancesByPK) + positiveBalance.add(mintedAmount) + positiveTotalThisMiningSessionBalance := s.getOrInitBalance(false, s.reverseT0ThisDurationDegradationReferenceTypeDetail(details, now), details.UUserID, balancesByPK) //nolint:lll // . + positiveTotalThisMiningSessionBalance.add(mintedAmount) +} + +//nolint:dupl,revive // Prefer decoupling. +func (s *balanceRecalculationTriggerStreamSource) slashT0ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + positiveBalance := s.getOrInitBalance(false, details.reverseT0TypeDetail(), details.UUserID, balancesByPK) + if positiveBalance.Amount.IsZero() { + return + } + aggressive := details.T0LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + var referenceAmount *coin.ICEFlake + if aggressive { + referenceAmount = s.getBalance(false, details.reverseT0AggressiveDegradationReferenceTypeDetail(), balancesByPK).Amount + } else { + referenceAmount = s.getBalance(false, lastXMiningSessionsReverseT0TypeDetail, balancesByPK).Amount + } + slashedAmount := s.calculateDegradation(elapsedDuration, referenceAmount, aggressive) + positiveBalance.subtract(slashedAmount) + if details.T0RollbackUsedAt == nil || (previous && details.T0LastMiningEndedAt.After(*now.Time) && details.T0RollbackUsedAt.Equal(*details.T0LastMiningStartedAt.Time)) { //nolint:lll // . + negativeBalance := s.getOrInitBalance(true, details.reverseT0TypeDetail(), details.UUserID, balancesByPK) + negativeBalance.add(slashedAmount) + } +} + +func (s *balanceRecalculationTriggerStreamSource) processDegradationForT0ReverseTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + if details.T0UserID == details.UUserID || + details.T0UserID == "" || + details.T0LastMiningEndedAt == nil { + return + } + isPositiveMining := details.T0LastMiningEndedAt.After(*now.Time) + s.processLastXPositiveMiningSessions(balancesByPK, false, details.reverseT0TypeDetail()+"/&", lastXMiningSessionsReverseT0TypeDetail, details.UUserID) + + aggressive := details.T0LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + referenceBalance := s.getBalance(false, details.reverseT0AggressiveDegradationReferenceTypeDetail(), balancesByPK) + if !isPositiveMining && aggressive && (referenceBalance == nil || referenceBalance.Amount.IsNil()) { + positiveBalance := s.getOrInitBalance(false, details.reverseT0TypeDetail(), details.UUserID, balancesByPK) + referenceBalance = s.getOrInitBalance(false, details.reverseT0AggressiveDegradationReferenceTypeDetail(), details.UUserID, balancesByPK) + referenceBalance.add(positiveBalance.Amount) + } + if isPositiveMining && referenceBalance != nil && !referenceBalance.Amount.IsZero() { + referenceBalance.Amount = coin.ZeroICEFlakes() + } +} + +func (s *balanceRecalculationTriggerStreamSource) reverseT0ThisDurationDegradationReferenceTypeDetail(details *BalanceRecalculationDetails, now *time.Time) string { //nolint:lll // . + return fmt.Sprintf("%v/&%v", details.reverseT0TypeDetail(), s.lastXMiningSessionsCollectingIntervalDateFormat(now)) +} + +const ( + lastXMiningSessionsReverseT0TypeDetail = reverseT0BalanceTypeDetail + "/0" +) + +func (d *BalanceRecalculationDetails) reverseT0TypeDetail() string { + return fmt.Sprintf("%v_%v", reverseT0BalanceTypeDetail, d.T0UserID) +} + +func (d *BalanceRecalculationDetails) reverseT0AggressiveDegradationReferenceTypeDetail() string { + return fmt.Sprintf("%v_", d.reverseT0TypeDetail()) +} + +func (d *BalanceRecalculationDetails) reverseT0Changed(typeDetail string) bool { + if !strings.HasPrefix(typeDetail, reverseT0BalanceTypeDetail+"_") { + return false + } + userID := strings.Replace(typeDetail, reverseT0BalanceTypeDetail+"_", "", 1) + userID = strings.Replace(userID, "_", "", 1) + userID = strings.Split(userID, "/")[0] + + return d.T0UserID != userID +} diff --git a/tokenomics/balance_recalculation_t0_total_no_pre_staking_bonus_amount.go b/tokenomics/balance_recalculation_t0_total_no_pre_staking_bonus_amount.go new file mode 100644 index 0000000..fd91bb2 --- /dev/null +++ b/tokenomics/balance_recalculation_t0_total_no_pre_staking_bonus_amount.go @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "fmt" + "strings" + stdlibtime "time" + + "github.com/ice-blockchain/wintr/coin" + "github.com/ice-blockchain/wintr/time" +) + +func (s *balanceRecalculationTriggerStreamSource) processPreviousIncompleteT0TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + if elapsedDuration == 0 || + details.T0UserID == details.UUserID || + details.T0UserID == "" { + return + } + if isPositiveMining := details.LastMiningEndedAt.After(*now.Time); isPositiveMining { // This means that the previous one was negative. + s.slashT0TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } else { + s.mintT0TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } +} + +func (s *balanceRecalculationTriggerStreamSource) rollbackT0TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + if details.T0UserID == details.UUserID || + details.T0UserID == "" || + details.LastMiningEndedAt.Before(*now.Time) { + return + } + negativeBalance := s.getBalance(true, details.t0TypeDetail(), balancesByPK) + if negativeBalance == nil || negativeBalance.Amount.IsZero() { + return + } + if details.RollbackUsedAt != nil { + positiveBalance := s.getOrInitBalance(false, details.t0TypeDetail(), details.UUserID, balancesByPK) + positiveBalance.add(negativeBalance.Amount) + } + negativeBalance.Amount = coin.ZeroICEFlakes() +} + +func (s *balanceRecalculationTriggerStreamSource) processT0TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, + details *BalanceRecalculationDetails, + now *time.Time, + elapsedDuration stdlibtime.Duration, +) { + if details.T0UserID == details.UUserID || + details.T0UserID == "" { + return + } + defer func() { + s.getBalance(false, lastXMiningSessionsT0TypeDetail, balancesByPK).Amount = nil + }() + if isPositiveMining := details.LastMiningEndedAt.After(*now.Time); isPositiveMining { + s.mintT0TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } else { + s.slashT0TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } +} + +func (s *balanceRecalculationTriggerStreamSource) mintT0TotalNoPreStakingBonusBalanceType( //nolint:revive // Nope. + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + if details.T0 == 0 { + return + } + params := &userMiningRateRecalculationParameters{T0: details.T0} + mintedAmount := s.calculateMintedStandardCoins(details.BaseMiningRate, params, elapsedDuration, true) + positiveBalance := s.getOrInitBalance(false, details.t0TypeDetail(), details.UUserID, balancesByPK) + positiveBalance.add(mintedAmount) + positiveTotalThisMiningSessionBalance := s.getOrInitBalance(false, s.t0ThisDurationDegradationReferenceTypeDetail(details, now), details.UUserID, balancesByPK) + positiveTotalThisMiningSessionBalance.add(mintedAmount) + if previous { + degradationReference := s.getOrInitBalance(false, degradationT0T1T2TotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + degradationReference.add(s.getBalance(false, lastXMiningSessionsT0TypeDetail, balancesByPK).Amount) + degradationReference.add(mintedAmount) + } +} + +//nolint:revive // Prefer decoupling. +func (s *balanceRecalculationTriggerStreamSource) slashT0TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + positiveBalance := s.getOrInitBalance(false, details.t0TypeDetail(), details.UUserID, balancesByPK) + if positiveBalance.Amount.IsZero() { + return + } + aggressive := details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + var referenceAmount *coin.ICEFlake + if aggressive { + referenceAmount = s.getBalance(false, details.t0AggressiveDegradationReferenceTypeDetail(), balancesByPK).Amount + } else { + referenceAmount = s.getBalance(false, lastXMiningSessionsT0TypeDetail, balancesByPK).Amount + } + negativeThisDuration := s.getOrInitBalance(true, s.lastXMiningSessionsThisDurationTypeDetail(previous), details.UUserID, balancesByPK) + slashedAmount := s.calculateDegradation(elapsedDuration, referenceAmount, aggressive) + positiveBalance.subtract(slashedAmount) + negativeThisDuration.add(slashedAmount) + if details.RollbackUsedAt == nil || (previous && details.RollbackUsedAt.Equal(*details.LastMiningStartedAt.Time)) { + negativeBalance := s.getOrInitBalance(true, details.t0TypeDetail(), details.UUserID, balancesByPK) + negativeBalance.add(slashedAmount) + } +} + +func (s *balanceRecalculationTriggerStreamSource) processDegradationForT0TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + if details.T0UserID == details.UUserID || + details.T0UserID == "" { + return + } + isPositiveMining := details.LastMiningEndedAt.After(*now.Time) + s.processLastXPositiveMiningSessions(balancesByPK, false, details.t0TypeDetail()+"/&", lastXMiningSessionsT0TypeDetail, details.UUserID) + if isPositiveMining { + degradationReference := s.getOrInitBalance(false, degradationT0T1T2TotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + degradationReference.add(s.getBalance(false, lastXMiningSessionsT0TypeDetail, balancesByPK).Amount) + } + + aggressive := details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + referenceBalance := s.getBalance(false, details.t0AggressiveDegradationReferenceTypeDetail(), balancesByPK) + if !isPositiveMining && aggressive && (referenceBalance == nil || referenceBalance.Amount.IsNil()) { + positiveBalance := s.getOrInitBalance(false, details.t0TypeDetail(), details.UUserID, balancesByPK) + referenceBalance = s.getOrInitBalance(false, details.t0AggressiveDegradationReferenceTypeDetail(), details.UUserID, balancesByPK) + referenceBalance.add(positiveBalance.Amount) + } + if isPositiveMining && referenceBalance != nil && !referenceBalance.Amount.IsZero() { + referenceBalance.Amount = coin.ZeroICEFlakes() + } +} + +func (s *balanceRecalculationTriggerStreamSource) t0ThisDurationDegradationReferenceTypeDetail(details *BalanceRecalculationDetails, now *time.Time) string { //nolint:lll // . + return fmt.Sprintf("%v/&%v", details.t0TypeDetail(), s.lastXMiningSessionsCollectingIntervalDateFormat(now)) +} + +const ( + lastXMiningSessionsT0TypeDetail = t0BalanceTypeDetail + "/0" +) + +func (d *BalanceRecalculationDetails) t0TypeDetail() string { + return fmt.Sprintf("%v_%v", t0BalanceTypeDetail, d.T0UserID) +} + +func (d *BalanceRecalculationDetails) t0AggressiveDegradationReferenceTypeDetail() string { + return fmt.Sprintf("%v_", d.t0TypeDetail()) +} + +func (d *BalanceRecalculationDetails) t0Changed(typeDetail string) bool { + if !strings.HasPrefix(typeDetail, t0BalanceTypeDetail+"_") { + return false + } + userID := strings.Replace(typeDetail, t0BalanceTypeDetail+"_", "", 1) + userID = strings.Replace(userID, "_", "", 1) + userID = strings.Split(userID, "/")[0] + + return d.T0UserID != userID +} diff --git a/tokenomics/balance_recalculation_t1_total_no_pre_staking_bonus_amount.go b/tokenomics/balance_recalculation_t1_total_no_pre_staking_bonus_amount.go new file mode 100644 index 0000000..42c5552 --- /dev/null +++ b/tokenomics/balance_recalculation_t1_total_no_pre_staking_bonus_amount.go @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: ice License 1.0 + +//nolint:dupl // . +package tokenomics + +import ( + "fmt" + stdlibtime "time" + + "github.com/ice-blockchain/wintr/coin" + "github.com/ice-blockchain/wintr/time" +) + +func (s *balanceRecalculationTriggerStreamSource) processPreviousIncompleteT1TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + if elapsedDuration == 0 { + return + } + if isPositiveMining := details.LastMiningEndedAt.After(*now.Time); isPositiveMining { // This means that the previous one was negative. + s.slashT1TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } else { + s.mintT1TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } +} + +func (s *balanceRecalculationTriggerStreamSource) rollbackT1TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + if details.LastMiningEndedAt.Before(*now.Time) { + return + } + negativeBalance := s.getBalance(true, t1BalanceTypeDetail, balancesByPK) + if negativeBalance == nil || negativeBalance.Amount.IsZero() { + return + } + if details.RollbackUsedAt != nil { + positiveBalance := s.getOrInitBalance(false, t1BalanceTypeDetail, details.UUserID, balancesByPK) + positiveBalance.add(negativeBalance.Amount) + } + negativeBalance.Amount = coin.ZeroICEFlakes() +} + +func (s *balanceRecalculationTriggerStreamSource) processT1TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, + aggregatedPendingBalances map[bool]*balance, + details *BalanceRecalculationDetails, + now *time.Time, + elapsedDuration stdlibtime.Duration, +) { + defer func() { + s.getBalance(false, lastXMiningSessionsT1TypeDetail, balancesByPK).Amount = nil + }() + if aggregatedPendingBalances != nil && aggregatedPendingBalances[false] != nil { + positiveBalance := s.getOrInitBalance(false, t1BalanceTypeDetail, details.UUserID, balancesByPK) + positiveBalance.subtract(aggregatedPendingBalances[false].Amount) + } + if aggregatedPendingBalances != nil && aggregatedPendingBalances[true] != nil { + negativeBalance := s.getOrInitBalance(true, t1BalanceTypeDetail, details.UUserID, balancesByPK) + negativeBalance.subtract(aggregatedPendingBalances[true].Amount) + } + if isPositiveMining := details.LastMiningEndedAt.After(*now.Time); isPositiveMining { + s.mintT1TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } else { + s.slashT1TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } +} + +func (s *balanceRecalculationTriggerStreamSource) mintT1TotalNoPreStakingBonusBalanceType( //nolint:revive // Nope. + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + if details.T1 == 0 { + return + } + params := &userMiningRateRecalculationParameters{T1: details.T1} + mintedAmount := s.calculateMintedStandardCoins(details.BaseMiningRate, params, elapsedDuration, true) + positiveBalance := s.getOrInitBalance(false, t1BalanceTypeDetail, details.UUserID, balancesByPK) + positiveBalance.add(mintedAmount) + positiveTotalThisMiningSessionBalance := s.getOrInitBalance(false, s.t1ThisDurationDegradationReferenceTypeDetail(now), details.UUserID, balancesByPK) + positiveTotalThisMiningSessionBalance.add(mintedAmount) + if previous { + degradationReference := s.getOrInitBalance(false, degradationT0T1T2TotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + degradationReference.add(s.getBalance(false, lastXMiningSessionsT1TypeDetail, balancesByPK).Amount) + degradationReference.add(mintedAmount) + } +} + +//nolint:revive // Prefer decoupling. +func (s *balanceRecalculationTriggerStreamSource) slashT1TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + positiveBalance := s.getOrInitBalance(false, t1BalanceTypeDetail, details.UUserID, balancesByPK) + if positiveBalance.Amount.IsZero() { + return + } + aggressive := details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + var referenceAmount *coin.ICEFlake + if aggressive { + referenceAmount = s.getBalance(false, aggressiveDegradationT1ReferenceBalanceTypeDetail, balancesByPK).Amount + } else { + referenceAmount = s.getBalance(false, lastXMiningSessionsT1TypeDetail, balancesByPK).Amount + } + negativeThisDuration := s.getOrInitBalance(true, s.lastXMiningSessionsThisDurationTypeDetail(previous), details.UUserID, balancesByPK) + slashedAmount := s.calculateDegradation(elapsedDuration, referenceAmount, aggressive) + positiveBalance.subtract(slashedAmount) + negativeThisDuration.add(slashedAmount) + if details.RollbackUsedAt == nil || (previous && details.RollbackUsedAt.Equal(*details.LastMiningStartedAt.Time)) { + negativeBalance := s.getOrInitBalance(true, t1BalanceTypeDetail, details.UUserID, balancesByPK) + negativeBalance.add(slashedAmount) + } +} + +func (s *balanceRecalculationTriggerStreamSource) processDegradationForT1TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + isPositiveMining := details.LastMiningEndedAt.After(*now.Time) + s.processLastXPositiveMiningSessions(balancesByPK, false, t1BalanceTypeDetail+"/&", lastXMiningSessionsT1TypeDetail, details.UUserID) + if isPositiveMining { + degradationReference := s.getOrInitBalance(false, degradationT0T1T2TotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + degradationReference.add(s.getBalance(false, lastXMiningSessionsT1TypeDetail, balancesByPK).Amount) + } + + aggressive := details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + referenceBalance := s.getBalance(false, aggressiveDegradationT1ReferenceBalanceTypeDetail, balancesByPK) + if !isPositiveMining && aggressive && (referenceBalance == nil || referenceBalance.Amount.IsNil()) { + positiveBalance := s.getOrInitBalance(false, t1BalanceTypeDetail, details.UUserID, balancesByPK) + referenceBalance = s.getOrInitBalance(false, aggressiveDegradationT1ReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + referenceBalance.add(positiveBalance.Amount) + } + if isPositiveMining && referenceBalance != nil && !referenceBalance.Amount.IsZero() { + referenceBalance.Amount = coin.ZeroICEFlakes() + } +} + +func (s *balanceRecalculationTriggerStreamSource) t1ThisDurationDegradationReferenceTypeDetail(now *time.Time) string { + return fmt.Sprintf("%v/&%v", t1BalanceTypeDetail, s.lastXMiningSessionsCollectingIntervalDateFormat(now)) +} + +const ( + lastXMiningSessionsT1TypeDetail = t1BalanceTypeDetail + "/0" +) diff --git a/tokenomics/balance_recalculation_t2_total_no_pre_staking_bonus_amount.go b/tokenomics/balance_recalculation_t2_total_no_pre_staking_bonus_amount.go new file mode 100644 index 0000000..bf8679a --- /dev/null +++ b/tokenomics/balance_recalculation_t2_total_no_pre_staking_bonus_amount.go @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: ice License 1.0 + +//nolint:dupl // . +package tokenomics + +import ( + "fmt" + stdlibtime "time" + + "github.com/ice-blockchain/wintr/coin" + "github.com/ice-blockchain/wintr/time" +) + +func (s *balanceRecalculationTriggerStreamSource) processPreviousIncompleteT2TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + if elapsedDuration == 0 { + return + } + if isPositiveMining := details.LastMiningEndedAt.After(*now.Time); isPositiveMining { // This means that the previous one was negative. + s.slashT2TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } else { + s.mintT2TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } +} + +func (s *balanceRecalculationTriggerStreamSource) rollbackT2TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + if details.LastMiningEndedAt.Before(*now.Time) { + return + } + negativeBalance := s.getBalance(true, t2BalanceTypeDetail, balancesByPK) + if negativeBalance == nil || negativeBalance.Amount.IsZero() { + return + } + if details.RollbackUsedAt != nil { + positiveAmount := s.getOrInitBalance(false, t2BalanceTypeDetail, details.UUserID, balancesByPK) + positiveAmount.add(negativeBalance.Amount) + } + negativeBalance.Amount = coin.ZeroICEFlakes() +} + +func (s *balanceRecalculationTriggerStreamSource) processT2TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, + aggregatedPendingBalances map[bool]*balance, + details *BalanceRecalculationDetails, + now *time.Time, + elapsedDuration stdlibtime.Duration, +) { + defer func() { + s.getBalance(false, lastXMiningSessionsT2TypeDetail, balancesByPK).Amount = nil + }() + if aggregatedPendingBalances != nil && aggregatedPendingBalances[false] != nil { + positiveBalance := s.getOrInitBalance(false, t2BalanceTypeDetail, details.UUserID, balancesByPK) + positiveBalance.subtract(aggregatedPendingBalances[false].Amount) + } + if aggregatedPendingBalances != nil && aggregatedPendingBalances[true] != nil { + negativeBalance := s.getOrInitBalance(true, t2BalanceTypeDetail, details.UUserID, balancesByPK) + negativeBalance.subtract(aggregatedPendingBalances[true].Amount) + } + if isPositiveMining := details.LastMiningEndedAt.After(*now.Time); isPositiveMining { + s.mintT2TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } else { + s.slashT2TotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } +} + +func (s *balanceRecalculationTriggerStreamSource) mintT2TotalNoPreStakingBonusBalanceType( //nolint:revive // Nope. + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + if details.T2 == 0 { + return + } + params := &userMiningRateRecalculationParameters{T2: details.T2} + mintedAmount := s.calculateMintedStandardCoins(details.BaseMiningRate, params, elapsedDuration, true) + positiveBalance := s.getOrInitBalance(false, t2BalanceTypeDetail, details.UUserID, balancesByPK) + positiveBalance.add(mintedAmount) + positiveTotalThisMiningSessionBalance := s.getOrInitBalance(false, s.t2ThisDurationDegradationReferenceTypeDetail(now), details.UUserID, balancesByPK) + positiveTotalThisMiningSessionBalance.add(mintedAmount) + if previous { + degradationReference := s.getOrInitBalance(false, degradationT0T1T2TotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + degradationReference.add(s.getBalance(false, lastXMiningSessionsT2TypeDetail, balancesByPK).Amount) + degradationReference.add(mintedAmount) + } +} + +//nolint:revive // Prefer decoupling. +func (s *balanceRecalculationTriggerStreamSource) slashT2TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + positiveBalance := s.getOrInitBalance(false, t2BalanceTypeDetail, details.UUserID, balancesByPK) + if positiveBalance.Amount.IsZero() { + return + } + aggressive := details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + var referenceAmount *coin.ICEFlake + if aggressive { + referenceAmount = s.getBalance(false, aggressiveDegradationT2ReferenceBalanceTypeDetail, balancesByPK).Amount + } else { + referenceAmount = s.getBalance(false, lastXMiningSessionsT2TypeDetail, balancesByPK).Amount + } + negativeThisDuration := s.getOrInitBalance(true, s.lastXMiningSessionsThisDurationTypeDetail(previous), details.UUserID, balancesByPK) + slashedAmount := s.calculateDegradation(elapsedDuration, referenceAmount, aggressive) + positiveBalance.subtract(slashedAmount) + negativeThisDuration.add(slashedAmount) + if details.RollbackUsedAt == nil || (previous && details.RollbackUsedAt.Equal(*details.LastMiningStartedAt.Time)) { + negativeBalance := s.getOrInitBalance(true, t2BalanceTypeDetail, details.UUserID, balancesByPK) + negativeBalance.add(slashedAmount) + } +} + +func (s *balanceRecalculationTriggerStreamSource) processDegradationForT2TotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + isPositiveMining := details.LastMiningEndedAt.After(*now.Time) + s.processLastXPositiveMiningSessions(balancesByPK, false, t2BalanceTypeDetail+"/&", lastXMiningSessionsT2TypeDetail, details.UUserID) + if isPositiveMining { + degradationReference := s.getOrInitBalance(false, degradationT0T1T2TotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + degradationReference.add(s.getBalance(false, lastXMiningSessionsT2TypeDetail, balancesByPK).Amount) + } + + aggressive := details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + referenceBalance := s.getBalance(false, aggressiveDegradationT2ReferenceBalanceTypeDetail, balancesByPK) + if !isPositiveMining && aggressive && (referenceBalance == nil || referenceBalance.Amount.IsNil()) { + positiveBalance := s.getOrInitBalance(false, t2BalanceTypeDetail, details.UUserID, balancesByPK) + referenceBalance = s.getOrInitBalance(false, aggressiveDegradationT2ReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + referenceBalance.add(positiveBalance.Amount) + } + if isPositiveMining && referenceBalance != nil && !referenceBalance.Amount.IsZero() { + referenceBalance.Amount = coin.ZeroICEFlakes() + } +} + +func (s *balanceRecalculationTriggerStreamSource) t2ThisDurationDegradationReferenceTypeDetail(now *time.Time) string { + return fmt.Sprintf("%v/&%v", t2BalanceTypeDetail, s.lastXMiningSessionsCollectingIntervalDateFormat(now)) +} + +const ( + lastXMiningSessionsT2TypeDetail = t2BalanceTypeDetail + "/0" +) diff --git a/tokenomics/balance_recalculation_test.go b/tokenomics/balance_recalculation_test.go new file mode 100644 index 0000000..8b2266e --- /dev/null +++ b/tokenomics/balance_recalculation_test.go @@ -0,0 +1,1118 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "fmt" + "math/rand" + "sort" + "testing" + stdlibtime "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ice-blockchain/wintr/coin" + "github.com/ice-blockchain/wintr/time" +) + +func TestRepository_CalculateDegradation(t *testing.T) { //nolint:funlen // . + t.Parallel() + rep := &repository{cfg: &config{ + RollbackNegativeMining: struct { + Available struct { + After stdlibtime.Duration `yaml:"after"` + Until stdlibtime.Duration `yaml:"until"` + } `yaml:"available"` + LastXMiningSessionsCollectingInterval stdlibtime.Duration `yaml:"lastXMiningSessionsCollectingInterval" mapstructure:"lastXMiningSessionsCollectingInterval"` //nolint:lll // . + AggressiveDegradationStartsAfter stdlibtime.Duration `yaml:"aggressiveDegradationStartsAfter"` + }{ + Available: struct { + After stdlibtime.Duration `yaml:"after"` + Until stdlibtime.Duration `yaml:"until"` + }{ + Until: 180 * stdlibtime.Minute, + }, + AggressiveDegradationStartsAfter: 90 * stdlibtime.Minute, + }, + MiningSessionDuration: struct { + Min stdlibtime.Duration `yaml:"min"` + Max stdlibtime.Duration `yaml:"max"` + WarnAboutExpirationAfter stdlibtime.Duration `yaml:"warnAboutExpirationAfter"` + }{ + Min: 90 * stdlibtime.Second, + Max: 180 * stdlibtime.Second, + }, + }} + now := time.Now() + const initialAmount = 1_000_000_000_000 + pace := 10 * stdlibtime.Millisecond + reference := coin.NewAmountUint64(initialAmount) + total := coin.NewAmountUint64(initialAmount) + expectedIterations := (rep.cfg.RollbackNegativeMining.Available.Until - rep.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter) / pace + expectedSlashedChunk := initialAmount / expectedIterations.Nanoseconds() + + iterations := 0 + for !total.IsZero() { + now = time.New(now.Add(pace)) + slashedAmount := rep.calculateDegradation(pace, reference, rand.Intn(2) == 1) //nolint:gosec // . + require.InDelta(t, slashedAmount.Uint64(), expectedSlashedChunk, float64(expectedSlashedChunk/100)) + total = total.Subtract(slashedAmount) + iterations++ + } + require.InDelta(t, expectedIterations.Nanoseconds(), iterations, float64(expectedIterations.Nanoseconds()/100)) + require.Less(t, iterations, int(expectedIterations.Nanoseconds())) +} + +func TestRepository_CalculateMiningRateSummaries(t *testing.T) { //nolint:funlen,maintidx // . + t.Parallel() + rep := &repository{cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: 24 * stdlibtime.Hour, + Child: stdlibtime.Hour, + }, + ReferralBonusMiningRates: struct { + T0 uint64 `yaml:"t0"` + T1 uint64 `yaml:"t1"` + T2 uint64 `yaml:"t2"` + }{ + T0: 25, + T1: 25, + T2: 5, + }, + }} + baseMiningRate := coin.UnsafeParseAmount("16000000000") + actual := rep.calculateMiningRateSummaries(baseMiningRate, &userMiningRateRecalculationParameters{ + T0: 1, + T1: 10, + T2: 10, + ExtraBonus: 100, + PreStakingBonus: 100, + PreStakingAllocation: 50, + }, nil, PositiveMiningRateType) + assert.EqualValues(t, &MiningRates[MiningRateSummary[coin.ICE]]{ //nolint:dupl // Intended. + Type: PositiveMiningRateType, + Base: &MiningRateSummary[coin.ICE]{ + Amount: baseMiningRate.UnsafeICE(), + }, + Standard: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("42000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 137, + T2: 25, + PreStaking: 0, + Extra: 50, + Total: 162, + }, + }, + PreStaking: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 137, + T2: 25, + PreStaking: 50, + Extra: 50, + Total: 425, + }, + }, + Total: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("126000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 50, + Extra: 100, + Total: 687, + }, + }, + TotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + PositiveTotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + }, actual) + actual = rep.calculateMiningRateSummaries(baseMiningRate, &userMiningRateRecalculationParameters{ + T0: 1, + T1: 10, + T2: 10, + ExtraBonus: 100, + PreStakingBonus: 500, + PreStakingAllocation: 10, + }, nil, PositiveMiningRateType) + assert.EqualValues(t, &MiningRates[MiningRateSummary[coin.ICE]]{ //nolint:dupl // Intended. + Type: PositiveMiningRateType, + Base: &MiningRateSummary[coin.ICE]{ + Amount: baseMiningRate.UnsafeICE(), + }, + Standard: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("75600000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 247, + T2: 45, + PreStaking: 0, + Extra: 90, + Total: 372, + }, + }, + PreStaking: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("50400000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 27, + T2: 5, + PreStaking: 50, + Extra: 10, + Total: 215, + }, + }, + Total: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("126000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 50, + Extra: 100, + Total: 687, + }, + }, + TotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + PositiveTotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + }, actual) + actual = rep.calculateMiningRateSummaries(baseMiningRate, &userMiningRateRecalculationParameters{ + T0: 1, + T1: 10, + T2: 10, + ExtraBonus: 100, + PreStakingBonus: 100, + PreStakingAllocation: 100, + }, nil, PositiveMiningRateType) + assert.EqualValues(t, &MiningRates[MiningRateSummary[coin.ICE]]{ //nolint:dupl // Wrong. + Type: PositiveMiningRateType, + Base: &MiningRateSummary[coin.ICE]{ + Amount: baseMiningRate.UnsafeICE(), + }, + PreStaking: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("168000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 100, + Extra: 100, + Total: 950, + }, + }, + Total: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("168000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 100, + Extra: 100, + Total: 950, + }, + }, + TotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + PositiveTotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + }, actual) + actual = rep.calculateMiningRateSummaries(baseMiningRate, &userMiningRateRecalculationParameters{ + T0: 1, + T1: 10, + T2: 10, + ExtraBonus: 100, + }, nil, PositiveMiningRateType) + assert.EqualValues(t, &MiningRates[MiningRateSummary[coin.ICE]]{ //nolint:dupl // Wrong. + Type: PositiveMiningRateType, + Base: &MiningRateSummary[coin.ICE]{ + Amount: baseMiningRate.UnsafeICE(), + }, + Standard: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + Total: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + TotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + PositiveTotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + }, actual) + actual = rep.calculateMiningRateSummaries(baseMiningRate, &userMiningRateRecalculationParameters{ + T0: 1, + T1: 10, + T2: 10, + ExtraBonus: 100, + PreStakingBonus: 500, + PreStakingAllocation: 10, + }, coin.UnsafeParseAmount("1111"), NoneMiningRateType) + assert.EqualValues(t, &MiningRates[MiningRateSummary[coin.ICE]]{ //nolint:dupl // Wrong. + Type: NoneMiningRateType, + Base: &MiningRateSummary[coin.ICE]{ + Amount: baseMiningRate.UnsafeICE(), + }, + Standard: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("0").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 247, + T2: 45, + PreStaking: 0, + Extra: 90, + Total: 0, + }, + }, + PreStaking: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("0").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 27, + T2: 5, + PreStaking: 50, + Extra: 10, + Total: 0, + }, + }, + Total: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("0").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 50, + Extra: 100, + Total: 0, + }, + }, + TotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("0").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 0, + }, + }, + PositiveTotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + }, actual) + actual = rep.calculateMiningRateSummaries(baseMiningRate, &userMiningRateRecalculationParameters{ + T0: 1, + T1: 10, + T2: 10, + ExtraBonus: 100, + PreStakingBonus: 500, + PreStakingAllocation: 10, + }, coin.UnsafeParseAmount("1000"), NegativeMiningRateType) + assert.EqualValues(t, &MiningRates[MiningRateSummary[coin.ICE]]{ //nolint:dupl // Wrong. + Type: NegativeMiningRateType, + Base: &MiningRateSummary[coin.ICE]{ + Amount: baseMiningRate.UnsafeICE(), + }, + Standard: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("900").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 247, + T2: 45, + PreStaking: 0, + Extra: 90, + Total: 0, + }, + }, + PreStaking: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("600").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 27, + T2: 5, + PreStaking: 50, + Extra: 10, + Total: 0, + }, + }, + Total: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("1500").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 50, + Extra: 100, + Total: 0, + }, + }, + TotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("1000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 0, + }, + }, + PositiveTotalNoPreStakingBonus: &MiningRateSummary[coin.ICE]{ + Amount: coin.UnsafeParseAmount("84000000000").UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: 275, + T2: 50, + PreStaking: 0, + Extra: 100, + Total: 425, + }, + }, + }, actual) +} + +//nolint:funlen,gocognit,revive,tparallel // A lot of assertions. +func TestRepository_CalculateCoins(t *testing.T) { + t.Parallel() + const someElapsedNanos = 7_777_777 + repo := &repository{ + cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: 24 * stdlibtime.Hour, + Child: stdlibtime.Hour, + }, + ReferralBonusMiningRates: struct { + T0 uint64 `yaml:"t0"` + T1 uint64 `yaml:"t1"` + T2 uint64 `yaml:"t2"` + }{ + T0: 25, + T1: 25, + T2: 5, + }, + }, + } + baseMiningRate := coin.UnsafeParseAmount("16000000000") + for _, test := range calculateCoinsTests() { //nolint:paralleltest // It's not working. + t.Run(test.name, func(t *testing.T) { + interval := repo.cfg.GlobalAggregationInterval.Child + if test.expectedStandardMiningRate != "" { + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStandardMiningRate), repo.calculateMintedStandardCoins(baseMiningRate, test.userMiningRateRecalculationParameters, interval, false)) //nolint:lll // . + } else { + assert.Nil(t, repo.calculateMintedStandardCoins(baseMiningRate, test.userMiningRateRecalculationParameters, interval, false)) + } + if test.expectedStandardMintedCoins != "" { + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStandardMintedCoins), repo.calculateMintedStandardCoins(baseMiningRate, test.userMiningRateRecalculationParameters, someElapsedNanos, false)) //nolint:lll // . + } else { + assert.Nil(t, repo.calculateMintedStandardCoins(baseMiningRate, test.userMiningRateRecalculationParameters, someElapsedNanos, false)) + } + if test.expectedStandardT0ReferralEarnings != "" { + params := *test.userMiningRateRecalculationParameters + params.T1 = 0 + params.T2 = 0 + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStandardT0ReferralEarnings), repo.calculateMintedStandardCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) //nolint:lll // . + } else { + params := *test.userMiningRateRecalculationParameters + params.T1 = 0 + params.T2 = 0 + assert.Nil(t, repo.calculateMintedStandardCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) + } + if test.expectedStandardT1ReferralEarnings != "" { + params := *test.userMiningRateRecalculationParameters + params.T0 = 0 + params.T2 = 0 + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStandardT1ReferralEarnings), repo.calculateMintedStandardCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) //nolint:lll // . + } else { + params := *test.userMiningRateRecalculationParameters + params.T0 = 0 + params.T2 = 0 + assert.Nil(t, repo.calculateMintedStandardCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) + } + if test.expectedStandardT2ReferralEarnings != "" { + params := *test.userMiningRateRecalculationParameters + params.T1 = 0 + params.T0 = 0 + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStandardT2ReferralEarnings), repo.calculateMintedStandardCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) //nolint:lll // . + } else { + params := *test.userMiningRateRecalculationParameters + params.T1 = 0 + params.T0 = 0 + assert.Nil(t, repo.calculateMintedStandardCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) + } + if test.expectedStakingMiningRate != "" { + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStakingMiningRate), repo.calculateMintedPreStakingCoins(baseMiningRate, test.userMiningRateRecalculationParameters, interval, false)) //nolint:lll // . + } else { + assert.Nil(t, repo.calculateMintedPreStakingCoins(baseMiningRate, test.userMiningRateRecalculationParameters, interval, false)) + } + if test.expectedStakingMintedCoins != "" { + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStakingMintedCoins), repo.calculateMintedPreStakingCoins(baseMiningRate, test.userMiningRateRecalculationParameters, someElapsedNanos, false)) //nolint:lll // . + } else { + assert.Nil(t, repo.calculateMintedPreStakingCoins(baseMiningRate, test.userMiningRateRecalculationParameters, someElapsedNanos, false)) + } + if test.expectedStakingT0ReferralEarnings != "" { + params := *test.userMiningRateRecalculationParameters + params.T1 = 0 + params.T2 = 0 + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStakingT0ReferralEarnings), repo.calculateMintedPreStakingCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) //nolint:lll // . + } else { + params := *test.userMiningRateRecalculationParameters + params.T1 = 0 + params.T2 = 0 + assert.Nil(t, repo.calculateMintedPreStakingCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) + } + if test.expectedStakingT1ReferralEarnings != "" { + params := *test.userMiningRateRecalculationParameters + params.T0 = 0 + params.T2 = 0 + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStakingT1ReferralEarnings), repo.calculateMintedPreStakingCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) //nolint:lll // . + } else { + params := *test.userMiningRateRecalculationParameters + params.T0 = 0 + params.T2 = 0 + assert.Nil(t, repo.calculateMintedPreStakingCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) + } + if test.expectedStakingT2ReferralEarnings != "" { + params := *test.userMiningRateRecalculationParameters + params.T1 = 0 + params.T0 = 0 + assert.EqualValues(t, coin.UnsafeParseAmount(test.expectedStakingT2ReferralEarnings), repo.calculateMintedPreStakingCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) //nolint:lll // . + } else { + params := *test.userMiningRateRecalculationParameters + params.T1 = 0 + params.T0 = 0 + assert.Nil(t, repo.calculateMintedPreStakingCoins(baseMiningRate, ¶ms, someElapsedNanos, true)) + } + }) + } +} + +//nolint:funlen // A lot of cases. +func calculateCoinsTests() []*struct { + *userMiningRateRecalculationParameters + name string + expectedStandardMiningRate string + expectedStandardMintedCoins string + expectedStandardT0ReferralEarnings string + expectedStandardT1ReferralEarnings string + expectedStandardT2ReferralEarnings string + expectedStakingMiningRate string + expectedStakingMintedCoins string + expectedStakingT0ReferralEarnings string + expectedStakingT1ReferralEarnings string + expectedStakingT2ReferralEarnings string +} { + return []*struct { + *userMiningRateRecalculationParameters + name string + expectedStandardMiningRate string + expectedStandardMintedCoins string + expectedStandardT0ReferralEarnings string + expectedStandardT1ReferralEarnings string + expectedStandardT2ReferralEarnings string + expectedStakingMiningRate string + expectedStakingMintedCoins string + expectedStakingT0ReferralEarnings string + expectedStakingT1ReferralEarnings string + expectedStakingT2ReferralEarnings string + }{ + // NO staking. + { + name: "no referrals active, no staking", + expectedStandardMiningRate: "16000000000", + expectedStandardMintedCoins: "34567", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + PreStakingBonus: 100, // Has no effect cuz there's 0 allocation. + }, + }, + { + name: "1 T0 referrals active, no staking", + expectedStandardMiningRate: "20000000000", + expectedStandardMintedCoins: "43209", + expectedStandardT0ReferralEarnings: "8641", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T0: 1, + PreStakingBonus: 100, // Has no effect cuz there's 0 allocation. + }, + }, + { + name: "1 T1 referrals active, no staking", + expectedStandardMiningRate: "20000000000", + expectedStandardMintedCoins: "43209", + expectedStandardT1ReferralEarnings: "8641", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T1: 1, + PreStakingBonus: 100, // Has no effect cuz there's 0 allocation. + }, + }, + { + name: "1 T2 referrals active, no staking", + expectedStandardMiningRate: "16800000000", + expectedStandardMintedCoins: "36296", + expectedStandardT2ReferralEarnings: "1728", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T2: 1, + PreStakingBonus: 100, // Has no effect cuz there's 0 allocation. + }, + }, + { + name: "a lot of T0,T1 & T2 referrals active, no staking", + expectedStandardMiningRate: "840000020000000000", + expectedStandardMintedCoins: "1814814676543", + expectedStandardT0ReferralEarnings: "8641", + expectedStandardT1ReferralEarnings: "86419744444", + expectedStandardT2ReferralEarnings: "1728394888888", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T0: 1, + T1: 10_000_000, + T2: 1_000_000_000, + PreStakingBonus: 100, // Has no effect cuz there's 0 allocation. + }, + }, + // 100% Staking. + { + name: "no referrals active, 100% staking", + expectedStakingMiningRate: "32000000000", + expectedStakingMintedCoins: "69135", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + PreStakingBonus: 100, + PreStakingAllocation: 100, + }, + }, + { + name: "1 T0 referrals active, 100% staking", + expectedStakingMiningRate: "40000000000", + expectedStakingMintedCoins: "86419", + expectedStakingT0ReferralEarnings: "17283", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T0: 1, + PreStakingBonus: 100, + PreStakingAllocation: 100, + }, + }, + { + name: "1 T1 referrals active, 100% staking", + expectedStakingMiningRate: "40000000000", + expectedStakingMintedCoins: "86419", + expectedStakingT1ReferralEarnings: "17283", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T1: 1, + PreStakingBonus: 100, + PreStakingAllocation: 100, + }, + }, + { + name: "1 T2 referrals active, 100% staking", + expectedStakingMiningRate: "33600000000", + expectedStakingMintedCoins: "72592", + expectedStakingT2ReferralEarnings: "3456", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T2: 1, + PreStakingBonus: 100, + PreStakingAllocation: 100, + }, + }, + { + name: "a lot of T0,T1 & T2 referrals active, 100% staking", + expectedStakingMiningRate: "1680000040000000000", + expectedStakingMintedCoins: "3629629353086", + expectedStakingT0ReferralEarnings: "17283", + expectedStakingT1ReferralEarnings: "172839488888", + expectedStakingT2ReferralEarnings: "3456789777777", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T0: 1, + T1: 10_000_000, + T2: 1_000_000_000, + PreStakingBonus: 100, + PreStakingAllocation: 100, + }, + }, + // 50% Staking. + { + name: "no referrals active, 50% staking", + expectedStandardMiningRate: "8000000000", + expectedStakingMiningRate: "16000000000", + expectedStakingMintedCoins: "34567", + expectedStandardMintedCoins: "17283", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + PreStakingBonus: 100, + PreStakingAllocation: 50, + }, + }, + { + name: "1 T0 referrals active, 50% staking", + expectedStandardMiningRate: "10000000000", + expectedStakingMiningRate: "20000000000", + expectedStandardMintedCoins: "21604", + expectedStakingMintedCoins: "43209", + expectedStandardT0ReferralEarnings: "4320", + expectedStakingT0ReferralEarnings: "8641", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T0: 1, + PreStakingBonus: 100, + PreStakingAllocation: 50, + }, + }, + { + name: "1 T1 referrals active, 50% staking", + expectedStandardMiningRate: "10000000000", + expectedStakingMiningRate: "20000000000", + expectedStandardMintedCoins: "21604", + expectedStakingMintedCoins: "43209", + expectedStandardT1ReferralEarnings: "4320", + expectedStakingT1ReferralEarnings: "8641", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T1: 1, + PreStakingBonus: 100, + PreStakingAllocation: 50, + }, + }, + { + name: "1 T2 referrals active, 50% staking", + expectedStandardMiningRate: "8400000000", + expectedStakingMiningRate: "16800000000", + expectedStandardMintedCoins: "18148", + expectedStakingMintedCoins: "36296", + expectedStandardT2ReferralEarnings: "864", + expectedStakingT2ReferralEarnings: "1728", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T2: 1, + PreStakingBonus: 100, + PreStakingAllocation: 50, + }, + }, + { + name: "a lot of T0,T1 & T2 referrals active, 50% staking", + expectedStandardMiningRate: "420000018000000000", + expectedStakingMiningRate: "840000036000000000", + expectedStandardMintedCoins: "907407355555", + expectedStakingMintedCoins: "1814814711111", + expectedStandardT0ReferralEarnings: "4320", + expectedStakingT0ReferralEarnings: "8641", + expectedStandardT1ReferralEarnings: "43209872222", + expectedStakingT1ReferralEarnings: "86419744444", + expectedStandardT2ReferralEarnings: "864197444444", + expectedStakingT2ReferralEarnings: "1728394888888", + userMiningRateRecalculationParameters: &userMiningRateRecalculationParameters{ + T0: 1, + T1: 10_000_000, + T2: 1_000_000_000, + ExtraBonus: 100, + PreStakingBonus: 100, + PreStakingAllocation: 50, + }, + }, + } +} + +//nolint:lll,funlen,maintidx // . +func TestBalanceRecalculationTriggerStreamSource_recalculateBalances(t *testing.T) { + t.Parallel() + source := &balanceRecalculationTriggerStreamSource{ + processor: &processor{ + repository: &repository{ + cfg: &config{ + RollbackNegativeMining: struct { + Available struct { + After stdlibtime.Duration `yaml:"after"` + Until stdlibtime.Duration `yaml:"until"` + } `yaml:"available"` + LastXMiningSessionsCollectingInterval stdlibtime.Duration `yaml:"lastXMiningSessionsCollectingInterval" mapstructure:"lastXMiningSessionsCollectingInterval"` + AggressiveDegradationStartsAfter stdlibtime.Duration `yaml:"aggressiveDegradationStartsAfter"` + }{ + Available: struct { + After stdlibtime.Duration `yaml:"after"` + Until stdlibtime.Duration `yaml:"until"` + }{ + After: 7 * 24 * stdlibtime.Hour, + Until: 60 * 24 * stdlibtime.Hour, + }, + LastXMiningSessionsCollectingInterval: 24 * stdlibtime.Hour, + AggressiveDegradationStartsAfter: 30 * 24 * stdlibtime.Hour, + }, + MiningSessionDuration: struct { + Min stdlibtime.Duration `yaml:"min"` + Max stdlibtime.Duration `yaml:"max"` + WarnAboutExpirationAfter stdlibtime.Duration `yaml:"warnAboutExpirationAfter"` + }{ + Min: 12 * stdlibtime.Hour, + Max: 24 * stdlibtime.Hour, + }, + ReferralBonusMiningRates: struct { + T0 uint64 `yaml:"t0"` + T1 uint64 `yaml:"t1"` + T2 uint64 `yaml:"t2"` + }{ + T0: 25, + T1: 25, + T2: 5, + }, + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: 24 * stdlibtime.Hour, + Child: stdlibtime.Hour, + }, + WorkerCount: 10, + }, + }, + }, + } + now := time.Now() + baseMiningRate := coin.NewAmountUint64(16 * uint64(coin.Denomination)) + userID, t0UserID, tMinus1UserID := uuid.NewString(), uuid.NewString(), uuid.NewString() + details := &BalanceRecalculationDetails{ + LastNaturalMiningStartedAt: now, + LastMiningStartedAt: now, + T0LastMiningStartedAt: now, + TMinus1LastMiningStartedAt: now, + LastMiningEndedAt: time.New(now.Add(24 * stdlibtime.Hour)), + T0LastMiningEndedAt: time.New(now.Add(24 * stdlibtime.Hour)), + TMinus1LastMiningEndedAt: time.New(now.Add(24 * stdlibtime.Hour)), + PreviousMiningEndedAt: nil, + T0PreviousMiningEndedAt: nil, + TMinus1PreviousMiningEndedAt: nil, + RollbackUsedAt: nil, + T0RollbackUsedAt: nil, + TMinus1RollbackUsedAt: nil, + BaseMiningRate: baseMiningRate, + UUserID: userID, + T0UserID: t0UserID, + TMinus1UserID: tMinus1UserID, + T0: 1, + T1: 1, + T2: 1, + ExtraBonus: 100, + } + balances := []*balanceRecalculationRow{{ + BalanceRecalculationDetails: details, + }} + now = time.New(now.Add(1 * stdlibtime.Hour)) + balancesForReplace, balancesForDelete, processingStoppedForUserIDs, dayOffStartedEvents, userIDs := source.recalculateBalances(now, balances) + expectedBalancesForReplace := []*balance{{ + UpdatedAt: now, + Amount: coin.NewAmountUint64(32_000_000_000), + UserID: userID, + TypeDetail: source.thisDurationDegradationReferenceTypeDetail(now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(4_000_000_000), + UserID: userID, + TypeDetail: balances[0].t0TypeDetail(), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(4_000_000_000), + UserID: userID, + TypeDetail: source.t0ThisDurationDegradationReferenceTypeDetail(balances[0].BalanceRecalculationDetails, now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(4_000_000_000), + UserID: userID, + TypeDetail: balances[0].reverseT0TypeDetail(), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(4_000_000_000), + UserID: userID, + TypeDetail: source.reverseT0ThisDurationDegradationReferenceTypeDetail(balances[0].BalanceRecalculationDetails, now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(800_000_000), + UserID: userID, + TypeDetail: balances[0].reverseTMinus1TypeDetail(), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(800_000_000), + UserID: userID, + TypeDetail: source.reverseTMinus1ThisDurationDegradationReferenceTypeDetail(balances[0].BalanceRecalculationDetails, now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(4_000_000_000), + UserID: userID, + TypeDetail: t1BalanceTypeDetail, + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(800_000_000), + UserID: userID, + TypeDetail: t2BalanceTypeDetail, + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(4_000_000_000), + UserID: userID, + TypeDetail: source.t1ThisDurationDegradationReferenceTypeDetail(now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(800_000_000), + UserID: userID, + TypeDetail: source.t2ThisDurationDegradationReferenceTypeDetail(now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(32_000_000_000), + UserID: userID, + TypeDetail: "", + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(40_800_000_000), + UserID: userID, + TypeDetail: fmt.Sprintf("/%v", now.Format(source.cfg.globalAggregationIntervalChildDateFormat())), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(40_800_000_000), + UserID: userID, + TypeDetail: fmt.Sprintf("@%v", now.Format(source.cfg.globalAggregationIntervalChildDateFormat())), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }} + sort.SliceStable(balancesForReplace, func(i, j int) bool { + return fmt.Sprint(balancesForReplace[i].Negative, balancesForReplace[i].Type, balancesForReplace[i].TypeDetail, balancesForReplace[i].UserID) < fmt.Sprint(balancesForReplace[j].Negative, balancesForReplace[j].Type, balancesForReplace[j].TypeDetail, balancesForReplace[j].UserID) + }) + sort.SliceStable(expectedBalancesForReplace, func(i, j int) bool { + return fmt.Sprint(expectedBalancesForReplace[i].Negative, expectedBalancesForReplace[i].Type, expectedBalancesForReplace[i].TypeDetail, expectedBalancesForReplace[i].UserID) < fmt.Sprint(expectedBalancesForReplace[j].Negative, expectedBalancesForReplace[j].Type, expectedBalancesForReplace[j].TypeDetail, expectedBalancesForReplace[j].UserID) + }) + assert.EqualValues(t, expectedBalancesForReplace, balancesForReplace) + assert.EqualValues(t, []*balance{}, balancesForDelete) + assert.EqualValues(t, map[string]*time.Time{}, processingStoppedForUserIDs) + assert.EqualValues(t, []*FreeMiningSessionStarted{}, dayOffStartedEvents) + assert.EqualValues(t, []string{userID}, userIDs) + + balances = balances[:0] + for i := range expectedBalancesForReplace { + if expectedBalancesForReplace[i].TypeDetail == fmt.Sprintf("@%v", now.Format(source.cfg.globalAggregationIntervalChildDateFormat())) { + continue + } + balances = append(balances, &balanceRecalculationRow{BalanceRecalculationDetails: details, B: expectedBalancesForReplace[i]}) + } + now = time.New(now.Add(1 * stdlibtime.Hour)) + balancesForReplace, balancesForDelete, processingStoppedForUserIDs, dayOffStartedEvents, userIDs = source.recalculateBalances(now, balances) + expectedBalancesForReplace = []*balance{{ + UpdatedAt: now, + Amount: coin.NewAmountUint64(64_000_000_000), + UserID: userID, + TypeDetail: source.thisDurationDegradationReferenceTypeDetail(now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(8_000_000_000), + UserID: userID, + TypeDetail: balances[0].t0TypeDetail(), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(8_000_000_000), + UserID: userID, + TypeDetail: source.t0ThisDurationDegradationReferenceTypeDetail(balances[0].BalanceRecalculationDetails, now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(8_000_000_000), + UserID: userID, + TypeDetail: balances[0].reverseT0TypeDetail(), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(8_000_000_000), + UserID: userID, + TypeDetail: source.reverseT0ThisDurationDegradationReferenceTypeDetail(balances[0].BalanceRecalculationDetails, now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(1_600_000_000), + UserID: userID, + TypeDetail: balances[0].reverseTMinus1TypeDetail(), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(1_600_000_000), + UserID: userID, + TypeDetail: source.reverseTMinus1ThisDurationDegradationReferenceTypeDetail(balances[0].BalanceRecalculationDetails, now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(8_000_000_000), + UserID: userID, + TypeDetail: t1BalanceTypeDetail, + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(1_600_000_000), + UserID: userID, + TypeDetail: t2BalanceTypeDetail, + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(8_000_000_000), + UserID: userID, + TypeDetail: source.t1ThisDurationDegradationReferenceTypeDetail(now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(1_600_000_000), + UserID: userID, + TypeDetail: source.t2ThisDurationDegradationReferenceTypeDetail(now), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(64_000_000_000), + UserID: userID, + TypeDetail: "", + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(40_800_000_000), + UserID: userID, + TypeDetail: fmt.Sprintf("/%v", now.Format(source.cfg.globalAggregationIntervalChildDateFormat())), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(40_800_000_000), + UserID: userID, + TypeDetail: fmt.Sprintf("/%v", now.Add(-1*stdlibtime.Hour).Format(source.cfg.globalAggregationIntervalChildDateFormat())), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(40_800_000_000), + UserID: userID, + TypeDetail: degradationT0T1T2TotalReferenceBalanceTypeDetail, + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }, { + UpdatedAt: now, + Amount: coin.NewAmountUint64(81_600_000_000), + UserID: userID, + TypeDetail: fmt.Sprintf("@%v", now.Format(source.cfg.globalAggregationIntervalChildDateFormat())), + Type: totalNoPreStakingBonusBalanceType, + Negative: false, + }} + sort.SliceStable(balancesForReplace, func(i, j int) bool { + return fmt.Sprint(balancesForReplace[i].Negative, balancesForReplace[i].Type, balancesForReplace[i].TypeDetail, balancesForReplace[i].UserID) < fmt.Sprint(balancesForReplace[j].Negative, balancesForReplace[j].Type, balancesForReplace[j].TypeDetail, balancesForReplace[j].UserID) + }) + sort.SliceStable(expectedBalancesForReplace, func(i, j int) bool { + return fmt.Sprint(expectedBalancesForReplace[i].Negative, expectedBalancesForReplace[i].Type, expectedBalancesForReplace[i].TypeDetail, expectedBalancesForReplace[i].UserID) < fmt.Sprint(expectedBalancesForReplace[j].Negative, expectedBalancesForReplace[j].Type, expectedBalancesForReplace[j].TypeDetail, expectedBalancesForReplace[j].UserID) + }) + assert.EqualValues(t, expectedBalancesForReplace, balancesForReplace) + assert.EqualValues(t, []*balance{}, balancesForDelete) + assert.EqualValues(t, map[string]*time.Time{}, processingStoppedForUserIDs) + assert.EqualValues(t, []*FreeMiningSessionStarted{}, dayOffStartedEvents) + assert.EqualValues(t, []string{userID}, userIDs) +} diff --git a/tokenomics/balance_recalculation_this_duration_total_no_pre_staking_bonus_amount.go b/tokenomics/balance_recalculation_this_duration_total_no_pre_staking_bonus_amount.go new file mode 100644 index 0000000..248f087 --- /dev/null +++ b/tokenomics/balance_recalculation_this_duration_total_no_pre_staking_bonus_amount.go @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "fmt" + stdlibtime "time" + + "github.com/ice-blockchain/wintr/time" +) + +func (s *balanceRecalculationTriggerStreamSource) processPreviousIncompleteThisDurationTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, perDurationTypeDetail string, +) { + if elapsedDuration == 0 { + return + } + if isPositiveMining := details.LastMiningEndedAt.After(*now.Time); isPositiveMining { // This means that the previous one was negative. + s.slashThisDurationTotalNoPreStakingBonusBalanceType(balancesByPK, details, perDurationTypeDetail, true) + } else { + s.mintThisDurationTotalNoPreStakingBonusBalanceType(balancesByPK, details, elapsedDuration, perDurationTypeDetail) + } +} + +func (s *balanceRecalculationTriggerStreamSource) processThisDurationTotalNoPreStakingBonusBalanceType( //nolint:revive // . + balancesByPK map[string]*balance, + aggregatedPendingBalances map[bool]*balance, + details *BalanceRecalculationDetails, + now *time.Time, + elapsedDuration stdlibtime.Duration, + perDurationTypeDetail string, +) { + if aggregatedPendingBalances != nil && aggregatedPendingBalances[false] != nil { + positiveBalance := s.getOrInitBalance(false, perDurationTypeDetail, details.UUserID, balancesByPK) + positiveBalance.add(aggregatedPendingBalances[false].Amount) + } + if aggregatedPendingBalances != nil && aggregatedPendingBalances[true] != nil { + negativeBalance := s.getOrInitBalance(true, perDurationTypeDetail, details.UUserID, balancesByPK) + negativeBalance.add(aggregatedPendingBalances[true].Amount) + } + if isPositiveMining := details.LastMiningEndedAt.After(*now.Time); isPositiveMining { + s.mintThisDurationTotalNoPreStakingBonusBalanceType(balancesByPK, details, elapsedDuration, perDurationTypeDetail) + } else { + s.slashThisDurationTotalNoPreStakingBonusBalanceType(balancesByPK, details, perDurationTypeDetail, false) + } +} + +func (s *balanceRecalculationTriggerStreamSource) mintThisDurationTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, elapsedDuration stdlibtime.Duration, perDurationTypeDetail string, +) { + params := &userMiningRateRecalculationParameters{ + T0: details.T0, + T1: details.T1, + T2: details.T2, + ExtraBonus: details.ExtraBonus, + } + mintedAmount := s.calculateMintedStandardCoins(details.BaseMiningRate, params, elapsedDuration, false) + positiveBalance := s.getOrInitBalance(false, perDurationTypeDetail, details.UUserID, balancesByPK) + positiveBalance.add(mintedAmount) +} + +func (s *balanceRecalculationTriggerStreamSource) slashThisDurationTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, perDurationTypeDetail string, previous bool, +) { + totalPositiveLastXMiningSessions := s.getBalance(true, s.lastXMiningSessionsThisDurationTypeDetail(previous), balancesByPK) + if totalPositiveLastXMiningSessions == nil || totalPositiveLastXMiningSessions.Amount.IsZero() { + if totalPositiveLastXMiningSessions != nil { + totalPositiveLastXMiningSessions.Amount = nil + } + + return + } + negativeBalance := s.getOrInitBalance(true, perDurationTypeDetail, details.UUserID, balancesByPK) + negativeBalance.add(totalPositiveLastXMiningSessions.Amount) + totalPositiveLastXMiningSessions.Amount = nil +} + +func (*balanceRecalculationTriggerStreamSource) lastXMiningSessionsThisDurationTypeDetail(previous bool) string { + return fmt.Sprintf("t0+t1+t2+total/%v/0", previous) +} diff --git a/tokenomics/balance_recalculation_total_no_pre_staking_bonus_amount.go b/tokenomics/balance_recalculation_total_no_pre_staking_bonus_amount.go new file mode 100644 index 0000000..8be2d56 --- /dev/null +++ b/tokenomics/balance_recalculation_total_no_pre_staking_bonus_amount.go @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "fmt" + stdlibtime "time" + + "github.com/ice-blockchain/wintr/coin" + "github.com/ice-blockchain/wintr/time" +) + +func (s *balanceRecalculationTriggerStreamSource) processPreviousIncompleteTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, +) { + if elapsedDuration == 0 { + return + } + if isPositiveMining := details.LastMiningEndedAt.After(*now.Time); isPositiveMining { // This means that the previous one was negative. + s.slashTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } else { + s.mintTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, true) + } +} + +func (s *balanceRecalculationTriggerStreamSource) rollbackTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + if details.LastMiningEndedAt.Before(*now.Time) { + return + } + negativeBalance := s.getBalance(true, "", balancesByPK) + if negativeBalance == nil || negativeBalance.Amount.IsZero() { + return + } + if details.RollbackUsedAt != nil { + positiveBalance := s.getOrInitBalance(false, "", details.UUserID, balancesByPK) + positiveBalance.add(negativeBalance.Amount) + } + negativeBalance.Amount = coin.ZeroICEFlakes() +} + +func (s *balanceRecalculationTriggerStreamSource) processTotalNoPreStakingBonusBalanceType( //nolint:funlen,gocognit // . + balancesByPK map[string]*balance, + aggregatedPendingBalances map[bool]*balance, + details *BalanceRecalculationDetails, + now *time.Time, + elapsedDuration stdlibtime.Duration, +) { + defer func() { + s.getBalance(false, lastXMiningSessionsTypeDetail, balancesByPK).Amount = nil + }() + isAggressiveDegradation := details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + isPositiveMining := details.LastMiningEndedAt.After(*now.Time) + if aggregatedPendingBalances != nil && aggregatedPendingBalances[false] != nil { + positiveBalance := s.getOrInitBalance(false, "", details.UUserID, balancesByPK) + positiveBalance.add(aggregatedPendingBalances[false].Amount) + if !isPositiveMining && isAggressiveDegradation { + referenceBalance := s.getOrInitBalance(false, aggressiveDegradationTotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + referenceBalance.add(aggregatedPendingBalances[false].Amount) + } + positiveTotalThisMiningSessionBalance := s.getOrInitBalance(!isPositiveMining && !isAggressiveDegradation, s.thisDurationDegradationReferenceTypeDetail(now), details.UUserID, balancesByPK) //nolint:lll // . + positiveTotalThisMiningSessionBalance.add(aggregatedPendingBalances[false].Amount) + } + if isPositiveMining { + s.mintTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } else { + s.slashTotalNoPreStakingBonusBalanceType(balancesByPK, details, now, elapsedDuration, false) + } + if aggregatedPendingBalances != nil && aggregatedPendingBalances[true] != nil { + positiveBalance := s.getOrInitBalance(false, "", details.UUserID, balancesByPK) + positiveBalance.subtract(aggregatedPendingBalances[true].Amount) + if !isPositiveMining && isAggressiveDegradation { + referenceBalance := s.getOrInitBalance(false, aggressiveDegradationTotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + referenceBalance.subtract(aggregatedPendingBalances[true].Amount) + } + positiveTotalThisMiningSessionBalance := s.getOrInitBalance(!isPositiveMining && !isAggressiveDegradation, s.thisDurationDegradationReferenceTypeDetail(now), details.UUserID, balancesByPK) //nolint:lll // . + positiveTotalThisMiningSessionBalance.subtract(aggregatedPendingBalances[true].Amount) + } +} + +func (s *balanceRecalculationTriggerStreamSource) mintTotalNoPreStakingBonusBalanceType( //nolint:revive // Nope. + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + params := &userMiningRateRecalculationParameters{ExtraBonus: details.ExtraBonus} + mintedAmount := s.calculateMintedStandardCoins(details.BaseMiningRate, params, elapsedDuration, false) + positiveBalance := s.getOrInitBalance(false, "", details.UUserID, balancesByPK) + positiveBalance.add(mintedAmount) + positiveTotalThisMiningSessionBalance := s.getOrInitBalance(false, s.thisDurationDegradationReferenceTypeDetail(now), details.UUserID, balancesByPK) + positiveTotalThisMiningSessionBalance.add(mintedAmount) + if previous { + degradationReference := s.getOrInitBalance(false, degradationT0T1T2TotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + degradationReference.add(s.getBalance(false, lastXMiningSessionsTypeDetail, balancesByPK).Amount) + degradationReference.add(mintedAmount) + } +} + +//nolint:revive // Not a problem here. +func (s *balanceRecalculationTriggerStreamSource) slashTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, elapsedDuration stdlibtime.Duration, previous bool, +) { + positiveBalance := s.getOrInitBalance(false, "", details.UUserID, balancesByPK) + if positiveBalance.Amount.IsZero() { + return + } + var slashedAmount *coin.ICEFlake + if details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.Available.Until).Before(*now.Time) { + slashedAmount = positiveBalance.Amount + } else if aggressive := details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time); aggressive { + slashedAmount = s.calculateDegradation(elapsedDuration, s.getBalance(false, aggressiveDegradationTotalReferenceBalanceTypeDetail, balancesByPK).Amount, aggressive) //nolint:lll // . + } else { + slashedAmount = s.calculateDegradation(elapsedDuration, s.getBalance(false, lastXMiningSessionsTypeDetail, balancesByPK).Amount, aggressive) + } + negativeThisDuration := s.getOrInitBalance(true, s.lastXMiningSessionsThisDurationTypeDetail(previous), details.UUserID, balancesByPK) + positiveBalance.subtract(slashedAmount) + negativeThisDuration.add(slashedAmount) + if details.RollbackUsedAt == nil || (previous && details.RollbackUsedAt.Equal(*details.LastMiningStartedAt.Time)) { + negativeBalance := s.getOrInitBalance(true, "", details.UUserID, balancesByPK) + negativeBalance.add(slashedAmount) + } +} + +func (s *balanceRecalculationTriggerStreamSource) processDegradationForTotalNoPreStakingBonusBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, now *time.Time, +) { + isPositiveMining := details.LastMiningEndedAt.After(*now.Time) + s.processLastXPositiveMiningSessions(balancesByPK, isPositiveMining, "/&", lastXMiningSessionsTypeDetail, details.UUserID) + if isPositiveMining { + degradationReference := s.getOrInitBalance(false, degradationT0T1T2TotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + degradationReference.add(s.getBalance(false, lastXMiningSessionsTypeDetail, balancesByPK).Amount) + } + + aggressive := details.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time) + referenceBalance := s.getBalance(false, aggressiveDegradationTotalReferenceBalanceTypeDetail, balancesByPK) + if !isPositiveMining && aggressive && (referenceBalance == nil || referenceBalance.Amount.IsNil()) { + positiveBalance := s.getOrInitBalance(false, "", details.UUserID, balancesByPK) + referenceBalance = s.getOrInitBalance(false, aggressiveDegradationTotalReferenceBalanceTypeDetail, details.UUserID, balancesByPK) + referenceBalance.add(positiveBalance.Amount) + } + if isPositiveMining && referenceBalance != nil && !referenceBalance.Amount.IsZero() { + referenceBalance.Amount = coin.ZeroICEFlakes() + } +} + +func (s *balanceRecalculationTriggerStreamSource) thisDurationDegradationReferenceTypeDetail(now *time.Time) string { + return fmt.Sprintf("/&%v", s.lastXMiningSessionsCollectingIntervalDateFormat(now)) +} + +const ( + lastXMiningSessionsTypeDetail = "/0" +) diff --git a/tokenomics/balance_recalculation_until_this_duration_total_no_pre_staking_bonus_amount.go b/tokenomics/balance_recalculation_until_this_duration_total_no_pre_staking_bonus_amount.go new file mode 100644 index 0000000..b1a42d2 --- /dev/null +++ b/tokenomics/balance_recalculation_until_this_duration_total_no_pre_staking_bonus_amount.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "github.com/ice-blockchain/wintr/coin" +) + +//nolint:funlen,gocognit,gocyclo,revive,cyclop // . +func (s *balanceRecalculationTriggerStreamSource) processTotalNoPreStakingBonusUntilThisDurationBalanceType( + balancesByPK map[string]*balance, details *BalanceRecalculationDetails, untilThisDurationTypeDetail, userID string, +) { + var positiveTotalAmount *coin.ICEFlake + positiveTotalNoPreStakingBonusBalance := s.getBalance(false, "", balancesByPK) + positiveTotalT0NoPreStakingBonusBalance := s.getBalance(false, details.t0TypeDetail(), balancesByPK) + positiveTotalT1NoPreStakingBonusBalance := s.getBalance(false, t1BalanceTypeDetail, balancesByPK) + positiveTotalT2NoPreStakingBonusBalance := s.getBalance(false, t2BalanceTypeDetail, balancesByPK) + positiveUntilThisDurationBalance := s.getBalance(false, untilThisDurationTypeDetail, balancesByPK) + if positiveTotalNoPreStakingBonusBalance != nil && !positiveTotalNoPreStakingBonusBalance.Amount.IsNil() { + positiveTotalAmount = positiveTotalAmount.Add(positiveTotalNoPreStakingBonusBalance.Amount) + } + if positiveTotalT0NoPreStakingBonusBalance != nil && !positiveTotalT0NoPreStakingBonusBalance.Amount.IsNil() { + positiveTotalAmount = positiveTotalAmount.Add(positiveTotalT0NoPreStakingBonusBalance.Amount) + } + if positiveTotalT1NoPreStakingBonusBalance != nil && !positiveTotalT1NoPreStakingBonusBalance.Amount.IsNil() { + positiveTotalAmount = positiveTotalAmount.Add(positiveTotalT1NoPreStakingBonusBalance.Amount) + } + if positiveTotalT2NoPreStakingBonusBalance != nil && !positiveTotalT2NoPreStakingBonusBalance.Amount.IsNil() { + positiveTotalAmount = positiveTotalAmount.Add(positiveTotalT2NoPreStakingBonusBalance.Amount) + } + if !positiveTotalAmount.IsNil() { + positiveUntilThisDurationBalance = s.getOrInitBalance(false, untilThisDurationTypeDetail, userID, balancesByPK) + positiveUntilThisDurationBalance.Amount = positiveTotalAmount + } else if positiveUntilThisDurationBalance != nil && !positiveUntilThisDurationBalance.Amount.IsNil() { + positiveUntilThisDurationBalance.Amount = coin.ZeroICEFlakes() + } + var negativeTotalAmount *coin.ICEFlake + negativeTotalNoPreStakingBonusBalance := s.getBalance(true, "", balancesByPK) + negativeTotalT0NoPreStakingBonusBalance := s.getBalance(true, details.t0TypeDetail(), balancesByPK) + negativeTotalT1NoPreStakingBonusBalance := s.getBalance(true, t1BalanceTypeDetail, balancesByPK) + negativeTotalT2NoPreStakingBonusBalance := s.getBalance(true, t2BalanceTypeDetail, balancesByPK) + negativeUntilThisDurationBalance := s.getBalance(true, untilThisDurationTypeDetail, balancesByPK) + if negativeTotalNoPreStakingBonusBalance != nil && !negativeTotalNoPreStakingBonusBalance.Amount.IsNil() { + negativeTotalAmount = negativeTotalAmount.Add(negativeTotalNoPreStakingBonusBalance.Amount) + } + if negativeTotalT0NoPreStakingBonusBalance != nil && !negativeTotalT0NoPreStakingBonusBalance.Amount.IsNil() { + negativeTotalAmount = negativeTotalAmount.Add(negativeTotalT0NoPreStakingBonusBalance.Amount) + } + if negativeTotalT1NoPreStakingBonusBalance != nil && !negativeTotalT1NoPreStakingBonusBalance.Amount.IsNil() { + negativeTotalAmount = negativeTotalAmount.Add(negativeTotalT1NoPreStakingBonusBalance.Amount) + } + if negativeTotalT2NoPreStakingBonusBalance != nil && !negativeTotalT2NoPreStakingBonusBalance.Amount.IsNil() { + negativeTotalAmount = negativeTotalAmount.Add(negativeTotalT2NoPreStakingBonusBalance.Amount) + } + if !negativeTotalAmount.IsNil() { + negativeUntilThisDurationBalance = s.getOrInitBalance(true, untilThisDurationTypeDetail, userID, balancesByPK) + negativeUntilThisDurationBalance.Amount = negativeTotalAmount + } else if negativeUntilThisDurationBalance != nil && !negativeUntilThisDurationBalance.Amount.IsNil() { + negativeUntilThisDurationBalance.Amount = coin.ZeroICEFlakes() + } +} diff --git a/tokenomics/balance_test.go b/tokenomics/balance_test.go new file mode 100644 index 0000000..a9cd87c --- /dev/null +++ b/tokenomics/balance_test.go @@ -0,0 +1,2918 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "fmt" + "testing" + stdlibtime "time" + + "github.com/stretchr/testify/assert" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/wintr/coin" + "github.com/ice-blockchain/wintr/time" +) + +//nolint:funlen,dupl,maintidx // . +func TestProcessBalanceHistory_ChildIsHour_ParentIsDay_Minus30MinutesTimezone(t *testing.T) { + t.Parallel() + repo := &repository{cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: 24 * stdlibtime.Hour, + Child: stdlibtime.Hour, + }, + }} + utcOffset := -870 * stdlibtime.Minute // -14:30. + location := stdlibtime.FixedZone(utcOffset.String(), int(utcOffset.Seconds())) + now := time.Now() + now = time.New(now.Add(-users.NanosSinceMidnight(now))) + adoptions := []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: nil, + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + preStakingSummaries := []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + Years: 5, + Allocation: 100, + }, + Bonus: 10000, + }} + childFormat := repo.cfg.globalAggregationIntervalChildDateFormat() + balances := []*balance{ + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(4*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(5*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(40000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(15*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + } + expected := []*BalanceHistoryEntry{{ + Time: now.Add(-24 * stdlibtime.Hour).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }}, + }, { + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }} + + actual := repo.processBalanceHistory(balances, true, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }, { + Time: now.Add(-24 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }, { + AchievedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + BaseMiningRate: coin.UnsafeParseAmount("4000000000"), + Milestone: 3, + }} + preStakingSummaries = []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + Years: 3, + Allocation: 50, + }, + Bonus: 100, + }, { + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + Years: 5, + Allocation: 100, + }, + Bonus: 200, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + }}, + }, { + Time: now.Add(-24 * stdlibtime.Hour).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(96000000000).UnsafeICE(), + amount: coin.NewAmountUint64(96000000000), + Negative: true, + Bonus: -2500, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 500, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: false, + Bonus: 1700, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + }, { + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: false, + Bonus: 1700, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) +} + +//nolint:funlen,dupl,maintidx // . +func TestProcessBalanceHistory_ChildIsHour_ParentIsDay_Plus30MinutesTimezone(t *testing.T) { + t.Parallel() + repo := &repository{cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: 24 * stdlibtime.Hour, + Child: stdlibtime.Hour, + }, + }} + utcOffset := 870 * stdlibtime.Minute // +14:30. + location := stdlibtime.FixedZone(utcOffset.String(), int(utcOffset.Seconds())) + now := time.Now() + now = time.New(now.Add(-users.NanosSinceMidnight(now))) + adoptions := []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: nil, + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + preStakingSummaries := []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + Years: 5, + Allocation: 100, + }, + Bonus: 10000, + }} + childFormat := repo.cfg.globalAggregationIntervalChildDateFormat() + balances := []*balance{ + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(4*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(5*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(40000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(15*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + } + expected := []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }}, + }, { + Time: now.Add(24 * stdlibtime.Hour).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }} + actual := repo.processBalanceHistory(balances, true, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.Add(24 * stdlibtime.Hour).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }, { + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }, { + AchievedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + BaseMiningRate: coin.UnsafeParseAmount("4000000000"), + Milestone: 3, + }} + preStakingSummaries = []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + Years: 3, + Allocation: 50, + }, + Bonus: 100, + }, { + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + Years: 5, + Allocation: 100, + }, + Bonus: 200, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.Add(24 * stdlibtime.Hour).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }, { + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) +} + +//nolint:funlen,dupl,maintidx // . +func TestProcessBalanceHistory_ChildIsHour_ParentIsDay_Minus45MinutesTimezone(t *testing.T) { + t.Parallel() + repo := &repository{cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: 24 * stdlibtime.Hour, + Child: stdlibtime.Hour, + }, + }} + utcOffset := -765 * stdlibtime.Minute // -12:45. + location := stdlibtime.FixedZone(utcOffset.String(), int(utcOffset.Seconds())) + now := time.Now() + now = time.New(now.Add(-users.NanosSinceMidnight(now))) + adoptions := []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: nil, + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + preStakingSummaries := []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + Years: 5, + Allocation: 100, + }, + Bonus: 10000, + }} + childFormat := repo.cfg.globalAggregationIntervalChildDateFormat() + balances := []*balance{ + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(4*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(5*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(40000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(15*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + } + expected := []*BalanceHistoryEntry{{ + Time: now.Add(-24 * stdlibtime.Hour).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }}, + }, { + Time: now.In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }} + actual := repo.processBalanceHistory(balances, true, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }, { + Time: now.Add(-24 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }, { + AchievedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + BaseMiningRate: coin.UnsafeParseAmount("4000000000"), + Milestone: 3, + }} + preStakingSummaries = []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + Years: 3, + Allocation: 50, + }, + Bonus: 100, + }, { + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + Years: 5, + Allocation: 100, + }, + Bonus: 200, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + }}, + }, { + Time: now.Add(-24 * stdlibtime.Hour).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(96000000000).UnsafeICE(), + amount: coin.NewAmountUint64(96000000000), + Negative: true, + Bonus: -2500, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 500, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: false, + Bonus: 1700, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + }, { + Time: now.In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: false, + Bonus: 1700, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) +} + +//nolint:funlen,dupl,maintidx // . +func TestProcessBalanceHistory_ChildIsHour_ParentIsDay_Plus45MinutesTimezone(t *testing.T) { + t.Parallel() + repo := &repository{cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: 24 * stdlibtime.Hour, + Child: stdlibtime.Hour, + }, + }} + utcOffset := 765 * stdlibtime.Minute // +12:45. + location := stdlibtime.FixedZone(utcOffset.String(), int(utcOffset.Seconds())) + now := time.Now() + now = time.New(now.Add(-users.NanosSinceMidnight(now))) + adoptions := []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: nil, + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + preStakingSummaries := []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + Years: 5, + Allocation: 100, + }, + Bonus: 10000, + }} + childFormat := repo.cfg.globalAggregationIntervalChildDateFormat() + balances := []*balance{ + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(4*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(5*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(40000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(15*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + } + expected := []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }}, + }, { + Time: now.Add(24 * stdlibtime.Hour).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }} + actual := repo.processBalanceHistory(balances, true, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.Add(24 * stdlibtime.Hour).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }, { + Time: now.In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }, { + AchievedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + BaseMiningRate: coin.UnsafeParseAmount("4000000000"), + Milestone: 3, + }} + preStakingSummaries = []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + Years: 3, + Allocation: 50, + }, + Bonus: 100, + }, { + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Minute)), + Years: 5, + Allocation: 100, + }, + Bonus: 200, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.Add(24 * stdlibtime.Hour).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }, { + Time: now.In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) +} + +//nolint:funlen,dupl,maintidx // . +func TestProcessBalanceHistory_ChildIsMinute_ParentIsHour_Minus30MinuteTimezone(t *testing.T) { + t.Parallel() + repo := &repository{cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: stdlibtime.Hour, + Child: stdlibtime.Minute, + }, + }} + utcOffset := -870 * stdlibtime.Minute // -14:30. + location := stdlibtime.FixedZone(utcOffset.String(), int(utcOffset.Seconds())) + now := time.Now() + now = time.New(now.Add(-users.NanosSinceMidnight(now))) + adoptions := []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: nil, + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + preStakingSummaries := []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + Years: 5, + Allocation: 100, + }, + Bonus: 10000, + }} + childFormat := repo.cfg.globalAggregationIntervalChildDateFormat() + balances := []*balance{ + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(4*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(5*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(40000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(15*repo.cfg.GlobalAggregationInterval.Parent).Format(childFormat)), + Negative: true, + }, + } + expected := []*BalanceHistoryEntry{{ + Time: now.Add(-15 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }}, + }, { + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(4040000000000).UnsafeICE(), + amount: coin.NewAmountUint64(4040000000000), + Negative: true, + Bonus: -25350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(4040000000000).UnsafeICE(), + amount: coin.NewAmountUint64(4040000000000), + Negative: true, + Bonus: -25350, + }, + }}, + }} + actual := repo.processBalanceHistory(balances, true, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(4040000000000).UnsafeICE(), + amount: coin.NewAmountUint64(4040000000000), + Negative: true, + Bonus: -50600, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(4040000000000).UnsafeICE(), + amount: coin.NewAmountUint64(4040000000000), + Negative: true, + Bonus: -50600, + }, + }}, + }, { + Time: now.Add(-15 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }, { + AchievedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + BaseMiningRate: coin.UnsafeParseAmount("4000000000"), + Milestone: 3, + }} + preStakingSummaries = []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + Years: 3, + Allocation: 50, + }, + Bonus: 100, + }, { + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + Years: 5, + Allocation: 100, + }, + Bonus: 200, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + }}, + }, { + Time: now.Add(-15 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(96000000000).UnsafeICE(), + amount: coin.NewAmountUint64(96000000000), + Negative: true, + Bonus: -2500, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 500, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: false, + Bonus: 1700, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + }, { + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: false, + Bonus: 1700, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) +} + +//nolint:funlen,dupl,maintidx // . +func TestProcessBalanceHistory_ChildIsMinute_ParentIsHour_Plus30MinuteTimezone(t *testing.T) { + t.Parallel() + repo := &repository{cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: stdlibtime.Hour, + Child: stdlibtime.Minute, + }, + }} + utcOffset := 690 * stdlibtime.Minute // +11:30. + location := stdlibtime.FixedZone(utcOffset.String(), int(utcOffset.Seconds())) + now := time.Now() + now = time.New(now.Add(-users.NanosSinceMidnight(now))) + adoptions := []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: nil, + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + preStakingSummaries := []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + Years: 5, + Allocation: 100, + }, + Bonus: 10000, + }} + childFormat := repo.cfg.globalAggregationIntervalChildDateFormat() + balances := []*balance{ + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(4*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(5*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(40000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(15*repo.cfg.GlobalAggregationInterval.Parent).Format(childFormat)), + Negative: true, + }, + } + expected := []*BalanceHistoryEntry{{ + Time: now.Add(11 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }}, + }, { + Time: now.Add(26 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }} + actual := repo.processBalanceHistory(balances, true, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.Add(26 * stdlibtime.Hour).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }, { + Time: now.Add(11 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }, { + AchievedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + BaseMiningRate: coin.UnsafeParseAmount("4000000000"), + Milestone: 3, + }} + preStakingSummaries = []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + Years: 3, + Allocation: 50, + }, + Bonus: 100, + }, { + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + Years: 5, + Allocation: 100, + }, + Bonus: 200, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.Add(26 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + }}, + }, { + Time: now.Add(11 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-30 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) +} + +//nolint:funlen,dupl,maintidx // . +func TestProcessBalanceHistory_ChildIsMinute_ParentIsHour_Minus45MinuteTimezone(t *testing.T) { + t.Parallel() + repo := &repository{cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: stdlibtime.Hour, + Child: stdlibtime.Minute, + }, + }} + utcOffset := -705 * stdlibtime.Minute // -11:45. + location := stdlibtime.FixedZone(utcOffset.String(), int(utcOffset.Seconds())) + now := time.Now() + now = time.New(now.Add(-users.NanosSinceMidnight(now))) + adoptions := []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: nil, + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + preStakingSummaries := []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + Years: 5, + Allocation: 100, + }, + Bonus: 10000, + }} + childFormat := repo.cfg.globalAggregationIntervalChildDateFormat() + balances := []*balance{ + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(4*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(5*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(40000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(15*repo.cfg.GlobalAggregationInterval.Parent).Format(childFormat)), + Negative: true, + }, + } + expected := []*BalanceHistoryEntry{{ + Time: now.Add(-12 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }}, + }, { + Time: now.In(location).Add(3 * repo.cfg.GlobalAggregationInterval.Parent).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(4040000000000).UnsafeICE(), + amount: coin.NewAmountUint64(4040000000000), + Negative: true, + Bonus: -25350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(4040000000000).UnsafeICE(), + amount: coin.NewAmountUint64(4040000000000), + Negative: true, + Bonus: -25350, + }, + }}, + }} + actual := repo.processBalanceHistory(balances, true, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(3 * repo.cfg.GlobalAggregationInterval.Parent).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(4040000000000).UnsafeICE(), + amount: coin.NewAmountUint64(4040000000000), + Negative: true, + Bonus: -50600, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(4040000000000).UnsafeICE(), + amount: coin.NewAmountUint64(4040000000000), + Negative: true, + Bonus: -50600, + }, + }}, + }, { + Time: now.Add(-12 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }, { + AchievedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + BaseMiningRate: coin.UnsafeParseAmount("4000000000"), + Milestone: 3, + }} + preStakingSummaries = []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + Years: 3, + Allocation: 50, + }, + Bonus: 100, + }, { + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + Years: 5, + Allocation: 100, + }, + Bonus: 200, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(3 * repo.cfg.GlobalAggregationInterval.Parent).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + }}, + }, { + Time: now.Add(-12 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-15 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(96000000000).UnsafeICE(), + amount: coin.NewAmountUint64(96000000000), + Negative: true, + Bonus: -2500, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 500, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: false, + Bonus: 1700, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: true, + Bonus: -1900, + }, + }, { + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(72000000000).UnsafeICE(), + amount: coin.NewAmountUint64(72000000000), + Negative: false, + Bonus: 1700, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) +} + +//nolint:funlen,dupl,maintidx // . +func TestProcessBalanceHistory_ChildIsMinute_ParentIsHour_Plus45MinuteTimezone(t *testing.T) { + t.Parallel() + repo := &repository{cfg: &config{ + GlobalAggregationInterval: struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + }{ + Parent: stdlibtime.Hour, + Child: stdlibtime.Minute, + }, + }} + utcOffset := 705 * stdlibtime.Minute // +11:45. + location := stdlibtime.FixedZone(utcOffset.String(), int(utcOffset.Seconds())) + now := time.Now() + now = time.New(now.Add(-users.NanosSinceMidnight(now))) + adoptions := []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: nil, + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + preStakingSummaries := []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + Years: 5, + Allocation: 100, + }, + Bonus: 10000, + }} + childFormat := repo.cfg.globalAggregationIntervalChildDateFormat() + balances := []*balance{ + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(2*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(3*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(8000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(4*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: false, + }, + { + Amount: coin.NewAmountUint64(32000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(5*repo.cfg.GlobalAggregationInterval.Child).Format(childFormat)), + Negative: true, + }, + { + Amount: coin.NewAmountUint64(40000000000), + TypeDetail: fmt.Sprintf("/%v", now.Add(15*repo.cfg.GlobalAggregationInterval.Parent).Format(childFormat)), + Negative: true, + }, + } + expected := []*BalanceHistoryEntry{{ + Time: now.Add(11 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }}, + }, { + Time: now.In(location).Add(26 * repo.cfg.GlobalAggregationInterval.Parent).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }} + actual := repo.processBalanceHistory(balances, true, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(1000 * repo.cfg.GlobalAggregationInterval.Child)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(26 * repo.cfg.GlobalAggregationInterval.Parent).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(40000000000).UnsafeICE(), + amount: coin.NewAmountUint64(40000000000), + Negative: true, + Bonus: -350, + }, + }}, + }, { + Time: now.Add(11 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) + + adoptions = []*Adoption[coin.ICEFlake]{{ + AchievedAt: time.New(now.Add(-24 * stdlibtime.Hour)), + BaseMiningRate: coin.UnsafeParseAmount("16000000000"), + Milestone: 1, + }, { + AchievedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + BaseMiningRate: coin.UnsafeParseAmount("8000000000"), + Milestone: 2, + }, { + AchievedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + BaseMiningRate: coin.UnsafeParseAmount("4000000000"), + Milestone: 3, + }} + preStakingSummaries = []*PreStakingSummary{{ + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + Years: 3, + Allocation: 50, + }, + Bonus: 100, + }, { + PreStaking: &PreStaking{ + CreatedAt: time.New(now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).Add(8 * stdlibtime.Second)), + Years: 5, + Allocation: 100, + }, + Bonus: 200, + }} + expected = []*BalanceHistoryEntry{{ + Time: now.In(location).Add(26 * repo.cfg.GlobalAggregationInterval.Parent).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(15 * repo.cfg.GlobalAggregationInterval.Parent).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(120000000000).UnsafeICE(), + amount: coin.NewAmountUint64(120000000000), + Negative: true, + Bonus: -3100, + }, + }}, + }, { + Time: now.Add(11 * repo.cfg.GlobalAggregationInterval.Parent).In(location).Add(-45 * stdlibtime.Minute), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + TimeSeries: []*BalanceHistoryEntry{{ + Time: now.Add(5 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(32000000000).UnsafeICE(), + amount: coin.NewAmountUint64(32000000000), + Negative: true, + Bonus: -300, + }, + }, { + Time: now.Add(4 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(8000000000).UnsafeICE(), + amount: coin.NewAmountUint64(8000000000), + Negative: false, + Bonus: -50, + }, + }, { + Time: now.Add(3 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }, { + Time: now.Add(2 * repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.Add(repo.cfg.GlobalAggregationInterval.Child).In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: true, + Bonus: -250, + }, + }, { + Time: now.In(location), + Balance: &BalanceHistoryBalanceDiff{ + Amount: coin.NewAmountUint64(24000000000).UnsafeICE(), + amount: coin.NewAmountUint64(24000000000), + Negative: false, + Bonus: 50, + }, + }}, + }} + actual = repo.processBalanceHistory(balances, false, utcOffset, adoptions, preStakingSummaries) + assert.EqualValues(t, expected, actual) +} diff --git a/tokenomics/blockchain_balance_synchronization.go b/tokenomics/blockchain_balance_synchronization.go new file mode 100644 index 0000000..ad484f1 --- /dev/null +++ b/tokenomics/blockchain_balance_synchronization.go @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + "strings" + stdlibtime "time" + + "github.com/cenkalti/backoff/v4" + "github.com/goccy/go-json" + "github.com/google/uuid" + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/wintr/coin" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) initializeBlockchainBalanceSynchronizationWorker(ctx context.Context, usr *users.User) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + workerIndex := usr.HashCode % r.cfg.WorkerCount + err := retry(ctx, func() error { + if err := r.initializeWorker(ctx, "blockchain_balance_synchronization_worker_", usr.ID, workerIndex); err != nil { + if errors.Is(err, storage.ErrRelationNotFound) { + return err + } + + return errors.Wrapf(backoff.Permanent(err), + "failed to initializeBlockchainBalanceSynchronizationWorker for userID:%v,workerIndex:%v", usr.ID, workerIndex) + } + + return nil + }) + + return errors.Wrapf(err, "permanently failed to initializeBlockchainBalanceSynchronizationWorker for userID:%v,workerIndex:%v", usr.ID, workerIndex) +} + +func (p *processor) startBlockchainBalanceSynchronizationTriggerSeedingStream(ctx context.Context) { + nilBodyForEachWorker := make([]any, p.cfg.WorkerCount) //nolint:makezero // Intended. + ticker := stdlibtime.NewTicker(blockchainBalanceSynchronizationSeedingStreamEmitFrequency) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + log.Error(errors.Wrap(sendMessagesConcurrently[any](ctx, p.sendBlockchainBalanceSynchronizationTriggerMessage, nilBodyForEachWorker), + "failed to sendMessagesConcurrently[sendBlockchainBalanceSynchronizationTriggerMessage]")) + case <-ctx.Done(): + return + } + } +} + +func (p *processor) sendBlockchainBalanceSynchronizationTriggerMessage(ctx context.Context, _ any) error { + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: uuid.NewString(), + Topic: p.cfg.MessageBroker.Topics[11].Name, + } + responder := make(chan error, 1) + defer close(responder) + p.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send `%v` message to broker", msg.Topic) +} + +func (s *blockchainBalanceSynchronizationTriggerStreamSource) Process(ignoredCtx context.Context, msg *messagebroker.Message) (err error) { + if ignoredCtx.Err() != nil { + return errors.Wrap(ignoredCtx.Err(), "unexpected deadline while processing message") + } + const deadline = 5 * stdlibtime.Minute + ctx, cancel := context.WithTimeout(context.Background(), deadline) + defer cancel() + rows, err := s.getLatestBalances(ctx, uint64(msg.Partition)) //nolint:contextcheck // We use context with longer deadline. + if err != nil || len(rows) == 0 { + return errors.Wrapf(err, "failed to getLatestBalances for workerIndex:%v", msg.Partition) + } + if err = s.updateBalances(ctx, rows); err != nil { //nolint:contextcheck // Intended. + return errors.Wrapf(err, "failed to updateBalances:%#v", rows) + } + if err = sendMessagesConcurrently(ctx, s.sendBalancesMessage, rows); err != nil { //nolint:contextcheck // We use context with longer deadline. + return errors.Wrapf(err, "failed to sendBalancesMessages for:%#v", rows) + } + + return errors.Wrapf(s.updateLastIterationFinishedAt(ctx, uint64(msg.Partition), rows), //nolint:contextcheck // We use context with longer deadline. + "failed to updateLastIterationFinishedAt for workerIndex:%v,rows:%#v", msg.Partition, rows) +} + +func (s *blockchainBalanceSynchronizationTriggerStreamSource) getLatestBalances( //nolint:funlen // . + ctx context.Context, workerIndex uint64, +) ([]*Balances[coin.ICEFlake], error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + batch, err := s.getLatestBalancesNewBatch(ctx, workerIndex) + if err != nil { + return nil, errors.Wrapf(err, "failed to getLatestBalancesNewBatch for workerIndex:%v", workerIndex) + } + if len(batch) == 0 { + return nil, nil + } + res := make([]*Balances[coin.ICEFlake], 0, len(batch)) + for _, row := range batch { + var standard, preStaking *coin.ICEFlake + switch row.PreStakingAllocation { + case 0: + standard = row.TotalNoPreStakingBonusBalanceAmount + case percentage100: + preStaking = row.TotalNoPreStakingBonusBalanceAmount. + MultiplyUint64(row.PreStakingBonus + percentage100). + DivideUint64(percentage100) + default: + standard = row.TotalNoPreStakingBonusBalanceAmount. + MultiplyUint64(percentage100 - row.PreStakingAllocation). + DivideUint64(percentage100) + preStaking = row.TotalNoPreStakingBonusBalanceAmount. + MultiplyUint64(row.PreStakingAllocation * (row.PreStakingBonus + percentage100)). + DivideUint64(percentage100 * percentage100) + } + res = append(res, &Balances[coin.ICEFlake]{ + Standard: standard, + PreStaking: preStaking, + UserID: row.UserID, + miningBlockchainAccountAddress: row.MiningBlockchainAccountAddress, + }) + } + + return res, nil +} + +type ( + latestBalanceSQLRow struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + TotalNoPreStakingBonusBalanceAmount *coin.ICEFlake + MiningBlockchainAccountAddress, UserID string + PreStakingAllocation, PreStakingBonus uint64 + } +) + +func (s *blockchainBalanceSynchronizationTriggerStreamSource) getLatestBalancesNewBatch( //nolint:funlen // . + ctx context.Context, workerIndex uint64, +) ([]*latestBalanceSQLRow, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + var ( + now = *time.Now().Time + limit = maxICEBlockchainConcurrentOperations / s.cfg.WorkerCount + typeDetails = make([]string, 0, 1+1) + params = make(map[string]any, 1+1) + ) + for i := stdlibtime.Duration(0); i <= 1; i++ { + dateFormat := now.Add(-1 * i * s.cfg.GlobalAggregationInterval.Child).Format(s.cfg.globalAggregationIntervalChildDateFormat()) + params[fmt.Sprintf("type_detail%v", i)] = fmt.Sprintf("@%v", dateFormat) + typeDetails = append(typeDetails, fmt.Sprintf(":type_detail%v", i)) + } + sql := fmt.Sprintf(` +SELECT IFNULL(IFNULL(x.amount,b.amount),'0'), + x.mining_blockchain_account_address, + x.user_id, + x.pre_staking_allocation, + st_b.bonus AS pre_staking_bonus +FROM (SELECT MAX(st.years) AS pre_staking_years, + MAX(st.allocation) AS pre_staking_allocation, + MAX(b.updated_at), + b.amount AS amount, + x.mining_blockchain_account_address, + x.user_id + FROM ( SELECT user_id, + mining_blockchain_account_address + FROM blockchain_balance_synchronization_worker_%[2]v + ORDER BY last_iteration_finished_at + LIMIT %[1]v ) x + LEFT JOIN pre_stakings_%[2]v st + ON st.user_id = x.user_id + LEFT JOIN balances_%[2]v b + ON b.user_id = x.user_id + AND b.negative = FALSE + AND b.type = %[3]v + AND b.type_detail IN (%[5]v) + GROUP BY x.user_id + ) x + LEFT JOIN pre_staking_bonuses st_b + ON st_b.years = x.pre_staking_years + LEFT JOIN balance_recalculation_worker_%[2]v not_started_yet_bal_worker + ON not_started_yet_bal_worker.user_id = x.user_id + AND (not_started_yet_bal_worker.last_iteration_finished_at IS NULL OR not_started_yet_bal_worker.last_mining_ended_at IS NULL) + LEFT JOIN balances_%[2]v b + ON b.user_id = not_started_yet_bal_worker.user_id + AND b.negative = FALSE + AND b.type = %[4]v + AND b.type_detail = ''`, limit, workerIndex, totalNoPreStakingBonusBalanceType, pendingXBalanceType, strings.Join(typeDetails, ",")) + res := make([]*latestBalanceSQLRow, 0, limit) + if err := s.db.PrepareExecuteTyped(sql, params, &res); err != nil { + return nil, errors.Wrapf(err, + "failed to select a batch of latest information about latest calculating balances for workerIndex:%v,params:%#v", workerIndex, params) + } + + return res, nil +} + +func (s *blockchainBalanceSynchronizationTriggerStreamSource) updateBalances( //nolint:funlen // Mostly mappings. + ctx context.Context, bs []*Balances[coin.ICEFlake], +) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + //nolint:godox // . + type blockchainMessage struct { // TODO: delete this and use the actual one. + AccountAddress string + ICEFlake string + PreStakingICEFlake string + } + values := make([]string, 0, len(bs)) + const balanceFields = 6 + params := make(map[string]any, len(bs)*balanceFields) + blockchainMessages := make([]*blockchainMessage, 0, len(bs)) + for ix, bal := range bs { + if bal.Standard.IsNil() { + bal.Standard = coin.ZeroICEFlakes() + } + if bal.PreStaking.IsNil() { + bal.PreStaking = coin.ZeroICEFlakes() + } + total := coin.New(bal.Standard.Add(bal.PreStaking)) + params[fmt.Sprintf("amount%v", ix)] = total.Amount + params[fmt.Sprintf("amount_w0%v", ix)] = total.AmountWord0 + params[fmt.Sprintf("amount_w1%v", ix)] = total.AmountWord1 + params[fmt.Sprintf("amount_w2%v", ix)] = total.AmountWord2 + params[fmt.Sprintf("amount_w3%v", ix)] = total.AmountWord3 + params[fmt.Sprintf("user_id%v", ix)] = bal.UserID + values = append(values, fmt.Sprintf("(:amount%[1]v,:amount_w0%[1]v,:amount_w1%[1]v,:amount_w2%[1]v,:amount_w3%[1]v,:user_id%[1]v)", ix)) + if bal.miningBlockchainAccountAddress != "" { + blockchainMessages = append(blockchainMessages, &blockchainMessage{ + AccountAddress: bal.miningBlockchainAccountAddress, + ICEFlake: bal.Standard.String(), + PreStakingICEFlake: bal.PreStaking.String(), + }) + } + } + sql := fmt.Sprintf(`REPLACE INTO balances (amount,amount_w0,amount_w1,amount_w2,amount_w3,user_id) VALUES %v`, strings.Join(values, ",")) + if _, err := storage.CheckSQLDMLResponse(s.db.PrepareExecute(sql, params)); err != nil { + return errors.Wrapf(err, "failed to replace into balances, params:%#v", params) + } + if len(blockchainMessages) != 0 { //nolint:revive,staticcheck // . + //nolint:godox // . + // TODO use blockchainMessages. + } + + return nil +} + +func (s *blockchainBalanceSynchronizationTriggerStreamSource) sendBalancesMessage(ctx context.Context, bs *Balances[coin.ICEFlake]) error { + valueBytes, err := json.MarshalContext(ctx, bs) + if err != nil { + return errors.Wrapf(err, "failed to marshal %#v", bs) + } + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: bs.UserID, + Topic: s.cfg.MessageBroker.Topics[4].Name, + Value: valueBytes, + } + responder := make(chan error, 1) + defer close(responder) + s.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send `%v` message to broker", msg.Topic) +} + +func (s *blockchainBalanceSynchronizationTriggerStreamSource) updateLastIterationFinishedAt( + ctx context.Context, workerIndex uint64, rows []*Balances[coin.ICEFlake], +) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + userIDs := make([]string, 0, len(rows)) + for i := range rows { + userIDs = append(userIDs, rows[i].UserID) + } + const table = "blockchain_balance_synchronization_worker_" + params := make(map[string]any, 1) + params["last_iteration_finished_at"] = time.Now() + err := s.updateWorkerFields(ctx, workerIndex, table, params, userIDs...) + + return errors.Wrapf(err, "failed to updateWorkerTimeField for workerIndex:%v,table:%q,params:%#v,userIDs:%#v", workerIndex, table, params, userIDs) +} + +func (r *repository) updateBlockchainBalanceSynchronizationWorkerBlockchainAccountAddress(ctx context.Context, usr *users.User) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + const table = "blockchain_balance_synchronization_worker_" + workerIndex := usr.HashCode % r.cfg.WorkerCount + params := make(map[string]any, 1) + params["mining_blockchain_account_address"] = usr.MiningBlockchainAccountAddress + err := r.updateWorkerFields(ctx, workerIndex, table, params, usr.ID) + + return errors.Wrapf(err, "failed to updateWorkerFields for workerIndex:%v,table:%q,params:%#v,userIDs:%#v", workerIndex, table, params, usr.ID) +} diff --git a/tokenomics/contract.go b/tokenomics/contract.go new file mode 100644 index 0000000..7bdc959 --- /dev/null +++ b/tokenomics/contract.go @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + _ "embed" + "io" + stdlibtime "time" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/go-tarantool-client" + "github.com/ice-blockchain/wintr/coin" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/multimedia/picture" + "github.com/ice-blockchain/wintr/time" +) + +// Public API. + +const ( + MaxPreStakingYears = 5 +) + +const ( + PositiveMiningRateType MiningRateType = "positive" + NegativeMiningRateType MiningRateType = "negative" + NoneMiningRateType MiningRateType = "none" +) + +var ( + ErrNotFound = storage.ErrNotFound + ErrRelationNotFound = storage.ErrRelationNotFound + ErrDuplicate = storage.ErrDuplicate + ErrNegativeMiningProgressDecisionRequired = errors.New("you have negative mining progress, please decide what to do with it") + ErrRaceCondition = errors.New("race condition") + ErrGlobalRankHidden = errors.New("global rank is hidden") + ErrDecreasingPreStakingAllocationOrYearsNotAllowed = errors.New("decreasing pre-staking allocation or years not allowed") +) + +type ( + MiningRateType string + AddBalanceCommand struct { + *Balances[coin.ICEFlake] + Negative *bool `json:"negative,omitempty" example:"false"` + EventID string `json:"eventId,omitempty" example:"some unique id"` + } + Miner struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + Balance *coin.ICE `json:"balance,omitempty" example:"12345.6334"` + UserID string `json:"userId,omitempty" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + Username string `json:"username,omitempty" example:"jdoe"` + ProfilePictureURL string `json:"profilePictureUrl,omitempty" example:"https://somecdn.com/p1.jpg"` + } + BalanceSummary struct { + Balances[coin.ICE] + } + Balances[DENOM coin.ICEFlake | coin.ICE] struct { + Total *DENOM `json:"total,omitempty" swaggertype:"string" example:"1,243.02"` + BaseFactor *DENOM `json:"baseFactor,omitempty" swaggerignore:"true" swaggertype:"string" example:"1,243.02"` + Standard *DENOM `json:"standard,omitempty" swaggertype:"string" example:"1,243.02"` + PreStaking *DENOM `json:"preStaking,omitempty" swaggertype:"string" example:"1,243.02"` + TotalNoPreStakingBonus *DENOM `json:"totalNoPreStakingBonus,omitempty" swaggertype:"string" example:"1,243.02"` + T1 *DENOM `json:"t1,omitempty" swaggertype:"string" example:"1,243.02"` + T2 *DENOM `json:"t2,omitempty" swaggertype:"string" example:"1,243.02"` + TotalReferrals *DENOM `json:"totalReferrals,omitempty" swaggertype:"string" example:"1,243.02"` + UserID string `json:"userId,omitempty" swaggerignore:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + miningBlockchainAccountAddress string + } + BalanceHistoryBalanceDiff struct { + Amount *coin.ICE `json:"amount" swaggertype:"string" example:"1,243.02"` + amount *coin.ICEFlake //nolint:revive // That's intended. + Bonus int64 `json:"bonus" example:"120"` + Negative bool `json:"negative" example:"true"` + } + BalanceHistoryEntry struct { + Time stdlibtime.Time `json:"time" swaggertype:"string" example:"2022-01-03T16:20:52.156534Z"` + Balance *BalanceHistoryBalanceDiff `json:"balance"` + TimeSeries []*BalanceHistoryEntry `json:"timeSeries"` + } + AdoptionSummary struct { + Milestones []*Adoption[coin.ICE] `json:"milestones"` + TotalActiveUsers uint64 `json:"totalActiveUsers" example:"11"` + } + AdoptionSnapshot struct { + *Adoption[coin.ICEFlake] + Before *Adoption[coin.ICEFlake] `json:"before,omitempty"` + } + Adoption[DENOM coin.ICEFlake | coin.ICE] struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + AchievedAt *time.Time `json:"achievedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + BaseMiningRate *DENOM `json:"baseMiningRate,omitempty" swaggertype:"string" example:"1,243.02"` + Milestone uint64 `json:"milestone,omitempty" example:"1"` + TotalActiveUsers uint64 `json:"totalActiveUsers,omitempty" example:"1"` + } + PreStakingSummary struct { + *PreStaking + Bonus uint64 `json:"bonus,omitempty" example:"100"` + } + PreStakingSnapshot struct { + *PreStakingSummary + Before *PreStakingSummary `json:"before,omitempty"` + } + PreStaking struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + CreatedAt *time.Time `json:"createdAt,omitempty" swaggerignore:"true" example:"2022-01-03T16:20:52.156534Z"` + UserID string `json:"userId,omitempty" swaggerignore:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + Years uint64 `json:"years,omitempty" example:"1"` + Allocation uint64 `json:"allocation,omitempty" example:"100"` + } + MiningRateBonuses struct { + T1 uint64 `json:"t1,omitempty" example:"100"` + T2 uint64 `json:"t2,omitempty" example:"200"` + PreStaking uint64 `json:"preStaking,omitempty" example:"300"` + Extra uint64 `json:"extra,omitempty" example:"300"` + Total uint64 `json:"total,omitempty" example:"300"` + } + MiningRateSummary[DENOM coin.ICEFlake | coin.ICE] struct { + Amount *DENOM `json:"amount,omitempty" example:"1,234,232.001" swaggertype:"string"` + Bonuses *MiningRateBonuses `json:"bonuses,omitempty"` + } + MiningRates[T coin.ICEFlake | MiningRateSummary[coin.ICE]] struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + Total *T `json:"total,omitempty"` + TotalNoPreStakingBonus *T `json:"totalNoPreStakingBonus,omitempty"` + PositiveTotalNoPreStakingBonus *T `json:"positiveTotalNoPreStakingBonus,omitempty"` + Standard *T `json:"standard,omitempty"` + PreStaking *T `json:"preStaking,omitempty"` + Base *T `json:"base,omitempty"` + Type MiningRateType `json:"type,omitempty"` + UserID string `json:"userId,omitempty" swaggerignore:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + } + MiningSummary struct { + MiningRates *MiningRates[MiningRateSummary[coin.ICE]] `json:"miningRates,omitempty"` + MiningSession *MiningSession `json:"miningSession,omitempty"` + ExtraBonusSummary + MiningStreak uint64 `json:"miningStreak,omitempty" example:"2"` + RemainingFreeMiningSessions uint64 `json:"remainingFreeMiningSessions,omitempty" example:"1"` + } + MiningSession struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + LastNaturalMiningStartedAt *time.Time `json:"lastNaturalMiningStartedAt,omitempty" example:"2022-01-03T16:20:52.156534Z" swaggerignore:"true"` + StartedAt *time.Time `json:"startedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + EndedAt *time.Time `json:"endedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + ResettableStartingAt *time.Time `json:"resettableStartingAt,omitempty" example:"2022-01-03T16:20:52.156534Z" ` + WarnAboutExpirationStartingAt *time.Time `json:"warnAboutExpirationStartingAt,omitempty" example:"2022-01-03T16:20:52.156534Z" ` + Free *bool `json:"free,omitempty" example:"true"` + UserID *string `json:"userId,omitempty" swaggerignore:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + MiningStreak uint64 `json:"miningStreak,omitempty" swaggerignore:"true" example:"11"` + } + ExtraBonusSummary struct { + UserID string `json:"userId,omitempty" swaggerignore:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + AvailableExtraBonus uint64 `json:"availableExtraBonus,omitempty" example:"2"` + ExtraBonusIndex uint64 `json:"extraBonusIndex,omitempty" swaggerignore:"true" example:"1"` + } + RankingSummary struct { + GlobalRank uint64 `json:"globalRank,omitempty" example:"12333"` + } + FreeMiningSessionStarted struct { + StartedAt *time.Time `json:"startedAt,omitempty"` + EndedAt *time.Time `json:"endedAt,omitempty"` + UserID string `json:"userId,omitempty" ` + ID string `json:"id,omitempty"` + RemainingFreeMiningSessions uint64 `json:"remainingFreeMiningSessions,omitempty"` + MiningStreak uint64 `json:"miningStreak,omitempty" example:"11"` + } + ReadRepository interface { + GetBalanceSummary(ctx context.Context, userID string) (*BalanceSummary, error) + GetRankingSummary(ctx context.Context, userID string) (*RankingSummary, error) + GetTopMiners(ctx context.Context, keyword string, limit, offset uint64) ([]*Miner, error) + GetMiningSummary(ctx context.Context, userID string) (*MiningSummary, error) + GetPreStakingSummary(ctx context.Context, userID string) (*PreStakingSummary, error) + GetBalanceHistory(ctx context.Context, userID string, start, end *time.Time, utcOffset stdlibtime.Duration, limit, offset uint64) ([]*BalanceHistoryEntry, error) //nolint:lll // . + GetAdoptionSummary(context.Context) (*AdoptionSummary, error) + } + WriteRepository interface { + StartNewMiningSession(ctx context.Context, ms *MiningSummary, rollbackNegativeMiningProgress *bool) error + ClaimExtraBonus(ctx context.Context, ebs *ExtraBonusSummary) error + StartOrUpdatePreStaking(context.Context, *PreStakingSummary) error + } + Repository interface { + io.Closer + + ReadRepository + WriteRepository + } + Processor interface { + Repository + CheckHealth(context.Context) error + } +) + +// Private API. + +const ( + applicationYamlKey = "tokenomics" + dayFormat, hourFormat, minuteFormat = "2006-01-02", "2006-01-02T15", "2006-01-02T15:04" + totalActiveUsersGlobalKey = "TOTAL_ACTIVE_USERS" + requestingUserIDCtxValueKey = "requestingUserIDCtxValueKey" + userHashCodeCtxValueKey = "userHashCodeCtxValueKey" + percentage100 = uint64(100) + registrationICEFlakeBonusAmount = 10 * uint64(coin.Denomination) + lastAdoptionMilestone = 6 + miningRatesRecalculationBatchSize = 100 + balanceRecalculationBatchSize = 100 + extraBonusProcessingBatchSize = 500 + maxICEBlockchainConcurrentOperations = 100000 + balanceCalculationProcessingSeedingStreamEmitFrequency = 3 * stdlibtime.Second + refreshMiningRatesProcessingSeedingStreamEmitFrequency = 3 * stdlibtime.Second + blockchainBalanceSynchronizationSeedingStreamEmitFrequency = 3 * stdlibtime.Second + extraBonusProcessingSeedingStreamEmitFrequency = 3 * stdlibtime.Second + requestDeadline = 25 * stdlibtime.Second +) + +const ( + t0BalanceTypeDetail = "t0" + t1BalanceTypeDetail = "t1" + t2BalanceTypeDetail = "t2" + degradationT0T1T2TotalReferenceBalanceTypeDetail = "@&" + aggressiveDegradationTotalReferenceBalanceTypeDetail = "_" + aggressiveDegradationT1ReferenceBalanceTypeDetail = t1BalanceTypeDetail + "_" + aggressiveDegradationT2ReferenceBalanceTypeDetail = t2BalanceTypeDetail + "_" + reverseT0BalanceTypeDetail = "&" + t0BalanceTypeDetail + reverseTMinus1BalanceTypeDetail = "&t-1" +) + +const ( + totalNoPreStakingBonusBalanceType balanceType = iota + pendingXBalanceType +) + +// . +var ( + //go:embed DDL.lua + ddl string +) + +type ( + balanceType int8 + userMiningRateRecalculationParameters struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // . + UserID users.UserID + T0, T1, T2, ExtraBonus, PreStakingAllocation, PreStakingBonus uint64 + } + user struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + CreatedAt *time.Time `json:"createdAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + UpdatedAt *time.Time `json:"updatedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + RollbackUsedAt *time.Time `json:"rollbackUsedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + LastNaturalMiningStartedAt *time.Time `json:"lastNaturalMiningStartedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + LastMiningStartedAt *time.Time `json:"lastMiningStartedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + LastMiningEndedAt *time.Time `json:"lastMiningEndedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + PreviousMiningStartedAt *time.Time `json:"previousMiningStartedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + PreviousMiningEndedAt *time.Time `json:"previousMiningEndedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + LastFreeMiningSessionAwardedAt *time.Time `json:"lastFreeMiningSessionAwardedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + UserID string `json:"userId,omitempty" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + ReferredBy string `json:"referredBy,omitempty" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + Username string `json:"username,omitempty" example:"jdoe"` + FirstName string `json:"firstName,omitempty" example:"John"` + LastName string `json:"lastName,omitempty" example:"Doe"` + ProfilePictureURL string `json:"profilePictureUrl,omitempty" example:"https://somecdn.com/p1.jpg"` + MiningBlockchainAccountAddress string `json:"miningBlockchainAccountAddress,omitempty" example:"0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + BlockchainAccountAddress string `json:"blockchainAccountAddress,omitempty" example:"0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + HashCode uint64 `json:"hashCode,omitempty" example:"1234567890"` + HideRanking bool `json:"hideRanking,omitempty" example:"false"` + Verified bool `json:"verified,omitempty" example:"false"` + } + balance struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + UpdatedAt *time.Time `json:"updatedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + Amount *coin.ICEFlake `json:"amount,omitempty" example:"1,235.777777777"` + UserID string `json:"userId,omitempty" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + TypeDetail string `json:"typeDetail,omitempty" example:"/2022-01-03"` + Type balanceType `json:"type,omitempty" example:"1"` + Negative bool `json:"negative,omitempty" example:"false"` + } + miningSummary struct { + LastNaturalMiningStartedAt *time.Time `json:"lastNaturalMiningStartedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + LastMiningStartedAt *time.Time `json:"lastMiningStartedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + LastMiningEndedAt *time.Time `json:"lastMiningEndedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + PreviousMiningStartedAt *time.Time `json:"previousMiningStartedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + PreviousMiningEndedAt *time.Time `json:"previousMiningEndedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + LastFreeMiningSessionAwardedAt *time.Time `json:"lastFreeMiningSessionAwardedAt,omitempty" example:"2022-01-03T16:20:52.156534Z"` + NegativeTotalNoPreStakingBonusBalanceAmount *coin.ICEFlake `json:"negativeTotalNoPreStakingBonusBalanceAmount,omitempty" example:"1,235.777777777"` + NegativeTotalT0NoPreStakingBonusBalanceAmount *coin.ICEFlake `json:"negativeTotalT0NoPreStakingBonusBalanceAmount,omitempty" example:"1,235.777777777"` + NegativeTotalT1NoPreStakingBonusBalanceAmount *coin.ICEFlake `json:"negativeTotalT1NoPreStakingBonusBalanceAmount,omitempty" example:"1,235.777777777"` + NegativeTotalT2NoPreStakingBonusBalanceAmount *coin.ICEFlake `json:"negativeTotalT2NoPreStakingBonusBalanceAmount,omitempty" example:"1,235.777777777"` + MiningStreak uint64 `json:"miningStreak,omitempty" example:"11"` + PreStakingYears uint64 `json:"preStakingYears,omitempty" example:"11"` + PreStakingAllocation uint64 `json:"preStakingAllocation,omitempty" example:"11"` + PreStakingBonus uint64 `json:"preStakingBonus,omitempty" example:"11"` + } + deviceMetadata struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + Before *deviceMetadata `json:"before,omitempty"` + UserID string `json:"userId,omitempty" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + TZ string `json:"tz,omitempty" example:"+03:00"` + } + viewedNews struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + UserID string `json:"userId,omitempty" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + NewsID string `json:"newsId,omitempty" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + } + + usersTableSource struct { + *processor + } + + globalTableSource struct { + *processor + } + + miningSessionsTableSource struct { + *processor + } + + addBalanceCommandsSource struct { + *processor + } + + viewedNewsSource struct { + *processor + } + + deviceMetadataTableSource struct { + *processor + } + + balanceRecalculationTriggerStreamSource struct { + *processor + } + + miningRatesRecalculationTriggerStreamSource struct { + *processor + } + + blockchainBalanceSynchronizationTriggerStreamSource struct { + *processor + } + + extraBonusProcessingTriggerStreamSource struct { + *processor + } + + repository struct { + cfg *config + shutdown func() error + db tarantool.Connector + mb messagebroker.Client + pictureClient picture.Client + } + + processor struct { + *repository + } + + config struct { + messagebroker.Config `mapstructure:",squash"` //nolint:tagliatelle // Nope. + AdoptionMilestoneSwitch struct { + ActiveUserMilestones []uint64 `yaml:"activeUserMilestones"` + ConsecutiveDurationsRequired uint64 `yaml:"consecutiveDurationsRequired"` + Duration stdlibtime.Duration `yaml:"duration"` + } `yaml:"adoptionMilestoneSwitch"` + ExtraBonuses struct { + FlatValues []uint64 `yaml:"flatValues"` + NewsSeenValues []uint64 `yaml:"newsSeenValues"` + MiningStreakValues []uint64 `yaml:"miningStreakValues"` + Duration stdlibtime.Duration `yaml:"duration"` + UTCOffsetDuration stdlibtime.Duration `yaml:"utcOffsetDuration" mapstructure:"utcOffsetDuration"` + ClaimWindow stdlibtime.Duration `yaml:"claimWindow"` + DelayedClaimPenaltyWindow stdlibtime.Duration `yaml:"delayedClaimPenaltyWindow"` + AvailabilityWindow stdlibtime.Duration `yaml:"availabilityWindow"` + TimeToAvailabilityWindow stdlibtime.Duration `yaml:"timeToAvailabilityWindow"` + } `yaml:"extraBonuses"` + RollbackNegativeMining struct { + Available struct { + After stdlibtime.Duration `yaml:"after"` + Until stdlibtime.Duration `yaml:"until"` + } `yaml:"available"` + LastXMiningSessionsCollectingInterval stdlibtime.Duration `yaml:"lastXMiningSessionsCollectingInterval" mapstructure:"lastXMiningSessionsCollectingInterval"` + AggressiveDegradationStartsAfter stdlibtime.Duration `yaml:"aggressiveDegradationStartsAfter"` + } `yaml:"rollbackNegativeMining"` + MiningSessionDuration struct { + Min stdlibtime.Duration `yaml:"min"` + Max stdlibtime.Duration `yaml:"max"` + WarnAboutExpirationAfter stdlibtime.Duration `yaml:"warnAboutExpirationAfter"` + } `yaml:"miningSessionDuration"` + ReferralBonusMiningRates struct { + T0 uint64 `yaml:"t0"` + T1 uint64 `yaml:"t1"` + T2 uint64 `yaml:"t2"` + } `yaml:"referralBonusMiningRates"` + ConsecutiveNaturalMiningSessionsRequiredFor1ExtraFreeArtificialMiningSession struct { + Min uint64 `yaml:"min"` + Max uint64 `yaml:"max"` + } `yaml:"consecutiveNaturalMiningSessionsRequiredFor1ExtraFreeArtificialMiningSession"` + GlobalAggregationInterval struct { + Parent stdlibtime.Duration `yaml:"parent"` + Child stdlibtime.Duration `yaml:"child"` + } `yaml:"globalAggregationInterval"` + WorkerCount uint64 `yaml:"workerCount"` + } +) diff --git a/tokenomics/extra_bonus.go b/tokenomics/extra_bonus.go new file mode 100644 index 0000000..8395b9f --- /dev/null +++ b/tokenomics/extra_bonus.go @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + "math/rand" + "strings" + "sync" + stdlibtime "time" + + "github.com/goccy/go-json" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + + "github.com/ice-blockchain/go-tarantool-client" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) ClaimExtraBonus(ctx context.Context, ebs *ExtraBonusSummary) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + now := time.Now() + bonus, err := r.getAvailableExtraBonus(ctx, now, ebs.UserID) + if err != nil { + return errors.Wrapf(err, "failed to getAvailableExtraBonus for userID:%v", ebs.UserID) + } + params := make(map[string]any, 5) //nolint:gomnd // There's 5 keys there. + params["user_id"] = ebs.UserID + params["now_nanos"] = now + params["extra_bonus"] = bonus.AvailableExtraBonus + params["duration"] = r.cfg.ExtraBonuses.Duration + params["claim_window"] = r.cfg.ExtraBonuses.ClaimWindow + sql := fmt.Sprintf(`UPDATE extra_bonus_processing_worker_%[1]v + SET extra_bonus = :extra_bonus, + extra_bonus_started_at = :now_nanos, + extra_bonus_ended_at = :now_nanos + :duration + WHERE user_id = :user_id + AND :now_nanos - IFNULL(extra_bonus_started_at,0) > :claim_window`, r.workerIndex(ctx)) + if err = storage.CheckSQLDMLErr(r.db.PrepareExecute(sql, params)); err != nil { + if errors.Is(err, storage.ErrNotFound) { + err = ErrDuplicate + } + + return errors.Wrapf(err, "failed to updated users to claim bonus for params:%#v", params) + } + *ebs = *bonus + + return nil +} + +//nolint:funlen,lll // . +func (r *repository) getAvailableExtraBonus(ctx context.Context, now *time.Time, userID string) (*ExtraBonusSummary, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + const networkLagDelta = 1.3 + params := make(map[string]any, 10) //nolint:gomnd // . + params["user_id"] = userID + params["now_nanos"] = now + params["duration"] = r.cfg.ExtraBonuses.Duration + params["utc_offset_duration"] = r.cfg.ExtraBonuses.UTCOffsetDuration + params["availability_window"] = r.cfg.ExtraBonuses.AvailabilityWindow + params["delayed_claim_penalty_window"] = r.cfg.ExtraBonuses.DelayedClaimPenaltyWindow + params["first_delayed_claim_penalty_window"] = stdlibtime.Duration(float64(r.cfg.ExtraBonuses.DelayedClaimPenaltyWindow.Nanoseconds()) * networkLagDelta) + params["time_to_availability_window"] = r.cfg.ExtraBonuses.TimeToAvailabilityWindow + params["claim_window"] = r.cfg.ExtraBonuses.ClaimWindow + params["worker_count"] = r.cfg.WorkerCount + sql := fmt.Sprintf(`SELECT bal_worker.last_mining_started_at, + bal_worker.last_mining_ended_at, + eb_worker.news_seen, + b.bonus, + (100 - (25 * ((CASE WHEN (:now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (e.extra_bonus_index * :duration)) - :time_to_availability_window - ((e.offset * :availability_window) / :worker_count)) < :first_delayed_claim_penalty_window THEN 0 ELSE (:now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (e.extra_bonus_index * :duration)) - :time_to_availability_window - ((e.offset * :availability_window) / :worker_count)) END)/:delayed_claim_penalty_window))) AS bonus_percentage_remaining, + :now_nanos - IFNULL(eb_worker.extra_bonus_started_at, 0) < :claim_window AS already_claimed + FROM extra_bonus_start_date sd + JOIN extra_bonus_processing_worker_%[1]v eb_worker + ON eb_worker.user_id = :user_id + JOIN extra_bonuses b + ON b.ix = (:now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - sd.value) / :duration + AND :now_nanos + (eb_worker.utc_offset * :utc_offset_duration) > sd.value + AND b.bonus > 0 + JOIN extra_bonuses_%[1]v e + ON b.ix = e.extra_bonus_index + AND :now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (e.extra_bonus_index * :duration)) - :time_to_availability_window - ((e.offset * :availability_window) / :worker_count) < :claim_window + AND :now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (e.extra_bonus_index * :duration)) - :time_to_availability_window - ((e.offset * :availability_window) / :worker_count) > 0 + JOIN balance_recalculation_worker_%[1]v bal_worker + ON bal_worker.user_id = eb_worker.user_id + WHERE sd.key = 0`, r.workerIndex(ctx)) + res := make([]*struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:tagliatelle,revive,nosnakecase // To insert we need asArray + LastMiningStartedAt, LastMiningEndedAt *time.Time + NewsSeen, FlatBonus, BonusPercentageRemaining uint64 + AlreadyClaimed bool + }, 0, 1) + if err := r.db.PrepareExecuteTyped(sql, params, &res); err != nil { + return nil, errors.Wrapf(err, "failed to select for available extra bonus for userID:%v", userID) + } + if len(res) == 0 { + return nil, ErrNotFound + } + if res[0].AlreadyClaimed { + return nil, ErrDuplicate + } + + return &ExtraBonusSummary{ + AvailableExtraBonus: r.calculateExtraBonus(res[0].FlatBonus, res[0].BonusPercentageRemaining, res[0].NewsSeen, r.calculateMiningStreak(now, res[0].LastMiningStartedAt, res[0].LastMiningEndedAt)), //nolint:lll // . + }, nil +} + +func (r *repository) calculateExtraBonus(flatBonus, bonusPercentageRemaining, newsSeen, miningStreak uint64) (extraBonus uint64) { + if flatBonus == 0 { + return 0 + } + if miningStreak >= uint64(len(r.cfg.ExtraBonuses.MiningStreakValues)) { + extraBonus += r.cfg.ExtraBonuses.MiningStreakValues[len(r.cfg.ExtraBonuses.MiningStreakValues)-1] + } else { + extraBonus += r.cfg.ExtraBonuses.MiningStreakValues[miningStreak] + } + if newsSeenBonusValues := r.cfg.ExtraBonuses.NewsSeenValues; newsSeen >= uint64(len(newsSeenBonusValues)) { + extraBonus += newsSeenBonusValues[len(newsSeenBonusValues)-1] + } else { + extraBonus += newsSeenBonusValues[newsSeen] + } + + return ((extraBonus + flatBonus) * bonusPercentageRemaining) / percentage100 +} + +func (r *repository) initializeExtraBonusWorkers() { + allWorkers := make(map[uint64]map[uint64]uint64, r.cfg.WorkerCount) + for extraBonusIndex := 0; extraBonusIndex < len(r.cfg.ExtraBonuses.FlatValues); extraBonusIndex++ { + offsets := make([]uint64, r.cfg.WorkerCount, r.cfg.WorkerCount) //nolint:gosimple // Prefer to be more descriptive. + for i := 0; i < len(offsets); i++ { + offsets[i] = uint64(i) + } + rand.New(rand.NewSource(time.Now().UnixNano())).Shuffle(len(offsets), func(i, j int) { //nolint:gosec // Not a problem here. + offsets[i], offsets[j] = offsets[j], offsets[i] + }) + for workerIndex := uint64(0); workerIndex < r.cfg.WorkerCount; workerIndex++ { + if _, found := allWorkers[workerIndex]; !found { + allWorkers[workerIndex] = make(map[uint64]uint64, len(r.cfg.ExtraBonuses.FlatValues)) + } + allWorkers[workerIndex][uint64(extraBonusIndex)] = offsets[workerIndex] + } + } + wg := new(sync.WaitGroup) + wg.Add(int(r.cfg.WorkerCount)) + for key, val := range allWorkers { + go func(workerIndex uint64, extraBonusesWorkerValues map[uint64]uint64) { + defer wg.Done() + r.mustPopulateExtraBonusWorker(workerIndex, extraBonusesWorkerValues) + }(key, val) + } + wg.Wait() +} + +func (r *repository) mustPopulateExtraBonusWorker(workerIndex uint64, extraBonusesWorkerValues map[uint64]uint64) { + values := make([]string, 0, r.cfg.WorkerCount) + for extraBonusIndex, offset := range extraBonusesWorkerValues { + values = append(values, fmt.Sprintf("(%v,%v)", extraBonusIndex, offset)) + } + sql := fmt.Sprintf("INSERT INTO extra_bonuses_%[1]v(extra_bonus_index,offset) VALUES %[2]v", workerIndex, strings.Join(values, ",")) + if err := storage.CheckSQLDMLErr(r.db.PrepareExecute(sql, map[string]any{})); err != nil && !errors.Is(err, ErrDuplicate) { + log.Panic(errors.Wrapf(err, "failed to initialize extra_bonuses_%[1]v", workerIndex)) + } +} + +type ( + extraBonusProcessingWorker struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // . + ExtraBonusStartedAt, ExtraBonusEndedAt *time.Time + UserID string + UTCOffset int64 + NewsSeen, ExtraBonus, LastExtraBonusIndexNotified uint64 // LastExtraBonusIndexNotified might be null, which is != 0. + } +) + +func (s *deviceMetadataTableSource) Process(ctx context.Context, msg *messagebroker.Message) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline while processing message") + } + if len(msg.Value) == 0 { + return nil + } + var dm deviceMetadata + if err := json.UnmarshalContext(ctx, msg.Value, &dm); err != nil { + return errors.Wrapf(err, "process: cannot unmarshall %v into %#v", string(msg.Value), &dm) + } + if dm.UserID == "" || dm.TZ == "" || (dm.Before != nil && dm.Before.TZ == dm.TZ) { + return nil + } + duration, err := stdlibtime.ParseDuration(strings.Replace(dm.TZ+"m", ":", "h", 1)) + if err != nil { + return errors.Wrapf(err, "invalid timezone:%#v", &dm) + } + workerIndex, err := s.getWorkerIndex(ctx, dm.UserID) + if err != nil { + return errors.Wrapf(err, "failed to getWorkerIndex for userID:%v", dm.UserID) + } + space := fmt.Sprintf("EXTRA_BONUS_PROCESSING_WORKER_%v", workerIndex) + pkIndex := fmt.Sprintf("pk_unnamed_%v_1", space) + ops := append(make([]tarantool.Op, 0, 1), tarantool.Op{Op: "=", Field: 3, Arg: duration / stdlibtime.Minute}) //nolint:gomnd // `utc_offset` column index. + + return errors.Wrapf(storage.CheckNoSQLDMLErr(s.db.UpdateTyped(space, pkIndex, tarantool.StringKey{S: dm.UserID}, ops, &[]*extraBonusProcessingWorker{})), + "failed to update users' timezone for %#v", &dm) +} + +func (s *viewedNewsSource) Process(ctx context.Context, msg *messagebroker.Message) error { //nolint:funlen // . + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline while processing message") + } + if len(msg.Value) == 0 { + return nil + } + var vn viewedNews + if err := json.UnmarshalContext(ctx, msg.Value, &vn); err != nil { + return errors.Wrapf(err, "process: cannot unmarshall %v into %#v", string(msg.Value), &vn) + } + if vn.UserID == "" { + return nil + } + if err := storage.CheckNoSQLDMLErr(s.db.InsertTyped("PROCESSED_SEEN_NEWS", &vn, &[]*viewedNews{})); err != nil { + return errors.Wrapf(err, "failed to insert PROCESSED_SEEN_NEWS:%#v)", &vn) + } + workerIndex, err := s.getWorkerIndex(ctx, vn.UserID) + if err != nil { + return errors.Wrapf(err, "failed to getWorkerIndex for userID:%v", vn.UserID) + } + space := fmt.Sprintf("EXTRA_BONUS_PROCESSING_WORKER_%v", workerIndex) + pkIndex := fmt.Sprintf("pk_unnamed_%v_1", space) + ops := append(make([]tarantool.Op, 0, 1), tarantool.Op{Op: "+", Field: 4, Arg: uint64(1)}) //nolint:gomnd // `news_seen` column index. + if err = storage.CheckNoSQLDMLErr(s.db.UpdateTyped(space, pkIndex, tarantool.StringKey{S: vn.UserID}, ops, &[]*extraBonusProcessingWorker{})); err != nil { + return multierror.Append( //nolint:wrapcheck // Not needed. + errors.Wrapf(err, "failed to update users' newsSeen count for %#v", &vn), + errors.Wrapf(storage.CheckNoSQLDMLErr(s.db.DeleteTyped("PROCESSED_SEEN_NEWS", "pk_unnamed_PROCESSED_SEEN_NEWS_1", []any{vn.UserID, vn.NewsID}, &[]*viewedNews{})), //nolint:lll // . + "[rollback]failed to delete PROCESSED_SEEN_NEWS(%v,%v)", vn.UserID, vn.NewsID), + ).ErrorOrNil() + } + + return nil +} diff --git a/tokenomics/extra_bonus_processing.go b/tokenomics/extra_bonus_processing.go new file mode 100644 index 0000000..e485aa7 --- /dev/null +++ b/tokenomics/extra_bonus_processing.go @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + stdlibtime "time" + + "github.com/cenkalti/backoff/v4" + "github.com/goccy/go-json" + "github.com/google/uuid" + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) initializeExtraBonusProcessingWorker(ctx context.Context, usr *users.User) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + workerIndex := usr.HashCode % r.cfg.WorkerCount + err := retry(ctx, func() error { + if err := r.initializeWorker(ctx, "extra_bonus_processing_worker_", usr.ID, workerIndex); err != nil { + if errors.Is(err, storage.ErrRelationNotFound) { + return err + } + + return errors.Wrapf(backoff.Permanent(err), + "failed to initializeExtraBonusProcessingWorker for userID:%v,workerIndex:%v", usr.ID, workerIndex) + } + + return nil + }) + + return errors.Wrapf(err, "permanently failed to initializeExtraBonusProcessingWorker for userID:%v,workerIndex:%v", usr.ID, workerIndex) +} + +func (p *processor) startExtraBonusProcessingTriggerSeedingStream(ctx context.Context) { + nilBodyForEachWorker := make([]any, p.cfg.WorkerCount) //nolint:makezero // Intended. + ticker := stdlibtime.NewTicker(extraBonusProcessingSeedingStreamEmitFrequency) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + log.Error(errors.Wrap(sendMessagesConcurrently[any](ctx, p.sendExtraBonusProcessingTriggerMessage, nilBodyForEachWorker), + "failed to sendMessagesConcurrently[sendExtraBonusProcessingTriggerMessage]")) + case <-ctx.Done(): + return + } + } +} + +func (p *processor) sendExtraBonusProcessingTriggerMessage(ctx context.Context, _ any) error { + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: uuid.NewString(), + Topic: p.cfg.MessageBroker.Topics[12].Name, + } + responder := make(chan error, 1) + defer close(responder) + p.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send `%v` message to broker", msg.Topic) +} + +func (s *extraBonusProcessingTriggerStreamSource) Process(ignoredCtx context.Context, msg *messagebroker.Message) (err error) { + if ignoredCtx.Err() != nil { + return errors.Wrap(ignoredCtx.Err(), "unexpected deadline while processing message") + } + const deadline = 5 * stdlibtime.Minute + ctx, cancel := context.WithTimeout(context.Background(), deadline) + defer cancel() + extraBonusIndex, availableExtraBonuses, err := s.getAvailableExtraBonuses(ctx, uint64(msg.Partition)) //nolint:contextcheck // Not needed here. + if err != nil { + return errors.Wrapf(err, "failed to getAvailableExtraBonuses for workerIndex:%v", uint64(msg.Partition)) + } + if err = sendMessagesConcurrently(ctx, s.sendAvailableDailyBonusMessage, availableExtraBonuses); err != nil { //nolint:contextcheck // Not needed here. + return errors.Wrapf(err, "failed to sendMessagesConcurrently[sendAvailableDailyBonusMessage] for availableExtraBonuses:%#v", availableExtraBonuses) + } + const table = "extra_bonus_processing_worker_" + params := make(map[string]any, 1) + params["last_extra_bonus_index_notified"] = extraBonusIndex + userIDs := make([]string, 0, len(availableExtraBonuses)) + for _, bonus := range availableExtraBonuses { + userIDs = append(userIDs, bonus.UserID) + } + + return errors.Wrapf(s.updateWorkerFields(ctx, uint64(msg.Partition), table, params, userIDs...), //nolint:contextcheck // Not needed here. + "failed to updateWorkerTimeField for workerIndex:%v,table:%q,params:%#v,userIDs:%#v", uint64(msg.Partition), table, params, userIDs) +} + +//nolint:funlen,lll // . +func (s *extraBonusProcessingTriggerStreamSource) getAvailableExtraBonuses( + ctx context.Context, workerIndex uint64, +) (extraBonusIndex uint64, availableExtraBonuses []*ExtraBonusSummary, err error) { + if ctx.Err() != nil { + return 0, nil, errors.Wrap(ctx.Err(), "unexpected deadline while processing message") + } + sql := fmt.Sprintf(`WITH sd AS (SELECT value FROM extra_bonus_start_date WHERE key = 0) + SELECT bal_worker.last_mining_started_at, + bal_worker.last_mining_ended_at, + eb_worker.user_id, + eb_worker.news_seen, + b.bonus, + (100 - (25 * ((CASE WHEN (:now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (b.ix * :duration)) - :time_to_availability_window - ((e.offset * :availability_window) / :worker_count)) < :first_delayed_claim_penalty_window THEN 0 ELSE (:now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (b.ix * :duration)) - :time_to_availability_window - ((e.offset * :availability_window) / :worker_count)) END)/:delayed_claim_penalty_window))) AS bonus_percentage_remaining, + b.ix + FROM extra_bonus_processing_worker_%[1]v eb_worker + JOIN balance_recalculation_worker_%[1]v bal_worker + ON bal_worker.user_id = eb_worker.user_id + JOIN sd + JOIN extra_bonuses b + ON b.ix = (:now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - sd.value) / :duration + AND :now_nanos + (eb_worker.utc_offset * :utc_offset_duration) > sd.value + AND b.bonus > 0 + JOIN extra_bonuses_%[1]v e + ON e.extra_bonus_index = b.ix + AND :now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (e.extra_bonus_index * :duration)) - :time_to_availability_window - ((e.offset * :availability_window) / :worker_count) < :claim_window + AND :now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (e.extra_bonus_index * :duration)) - :time_to_availability_window - ((e.offset * :availability_window) / :worker_count) > 0 + WHERE (eb_worker.last_extra_bonus_index_notified IS NULL OR eb_worker.last_extra_bonus_index_notified < b.ix) + AND :now_nanos - IFNULL(eb_worker.extra_bonus_started_at, 0) > :claim_window + LIMIT %[2]v`, workerIndex, extraBonusProcessingBatchSize) + now := time.Now() + const networkLagDelta = 1.3 + params := make(map[string]any, 9) //nolint:gomnd // . + params["now_nanos"] = now + params["duration"] = s.cfg.ExtraBonuses.Duration + params["utc_offset_duration"] = s.cfg.ExtraBonuses.UTCOffsetDuration + params["availability_window"] = s.cfg.ExtraBonuses.AvailabilityWindow + params["time_to_availability_window"] = s.cfg.ExtraBonuses.TimeToAvailabilityWindow + params["claim_window"] = s.cfg.ExtraBonuses.ClaimWindow + params["worker_count"] = s.cfg.WorkerCount + params["delayed_claim_penalty_window"] = s.cfg.ExtraBonuses.DelayedClaimPenaltyWindow + params["first_delayed_claim_penalty_window"] = stdlibtime.Duration(float64(s.cfg.ExtraBonuses.DelayedClaimPenaltyWindow.Nanoseconds()) * networkLagDelta) + resp := make([]*struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:tagliatelle,revive,nosnakecase // . + LastMiningStartedAt, LastMiningEndedAt *time.Time + UserID string + NewsSeen, FlatBonus, BonusPercentageRemaining, ExtraBonusIndex uint64 + }, 0, extraBonusProcessingBatchSize) + if err = s.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return 0, nil, errors.Wrapf(err, "failed to select for availableExtraBonuses for workerIndex:%v", workerIndex) + } + if len(resp) != 0 { + extraBonusIndex = resp[0].ExtraBonusIndex + } + availableExtraBonuses = make([]*ExtraBonusSummary, 0, len(resp)) + for _, row := range resp { + availableExtraBonuses = append(availableExtraBonuses, &ExtraBonusSummary{ + UserID: row.UserID, + AvailableExtraBonus: s.calculateExtraBonus(row.FlatBonus, row.BonusPercentageRemaining, row.NewsSeen, s.calculateMiningStreak(now, row.LastMiningStartedAt, row.LastMiningEndedAt)), //nolint:lll // . + ExtraBonusIndex: row.ExtraBonusIndex, + }) + } + + return extraBonusIndex, availableExtraBonuses, nil +} + +func (s *extraBonusProcessingTriggerStreamSource) sendAvailableDailyBonusMessage(ctx context.Context, ebs *ExtraBonusSummary) error { + valueBytes, err := json.MarshalContext(ctx, ebs) + if err != nil { + return errors.Wrapf(err, "failed to marshal %#v", ebs) + } + + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: ebs.UserID, + Topic: s.cfg.MessageBroker.Topics[7].Name, + Value: valueBytes, + } + + responder := make(chan error, 1) + defer close(responder) + s.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send %v message to broker, msg:%#v", msg.Topic, ebs) +} diff --git a/tokenomics/fixture/contract.go b/tokenomics/fixture/contract.go new file mode 100644 index 0000000..755c355 --- /dev/null +++ b/tokenomics/fixture/contract.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: ice License 1.0 + +package fixture + +// Public API. + +const ( + TestConnectorsOrder = 0 +) + +const ( + All StartLocalTestEnvironmentType = "all" + DB StartLocalTestEnvironmentType = "db" + MB StartLocalTestEnvironmentType = "mb" +) + +type ( + StartLocalTestEnvironmentType string +) + +// Private API. + +const ( + applicationYAMLKey = "tokenomics" +) diff --git a/tokenomics/fixture/fixture.go b/tokenomics/fixture/fixture.go new file mode 100644 index 0000000..a6d421f --- /dev/null +++ b/tokenomics/fixture/fixture.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: ice License 1.0 + +package fixture + +import ( + "testing" + + "github.com/testcontainers/testcontainers-go" + + connectorsfixture "github.com/ice-blockchain/wintr/connectors/fixture" + messagebrokerfixture "github.com/ice-blockchain/wintr/connectors/message_broker/fixture" + storagefixture "github.com/ice-blockchain/wintr/connectors/storage/fixture" +) + +func StartLocalTestEnvironment(tp StartLocalTestEnvironmentType) { + var connectors []connectorsfixture.TestConnector + switch tp { + case DB: + connectors = append(connectors, newDBConnector()) + case MB: + connectors = append(connectors, newMBConnector()) + case All: + connectors = WTestConnectors() + default: + connectors = WTestConnectors() + } + connectorsfixture. + NewTestRunner(applicationYAMLKey, nil, connectors...). + StartConnectorsIndefinitely() +} + +//nolint:gocritic // Because that's exactly what we want. +func RunTests( + m *testing.M, + dbConnector *storagefixture.TestConnector, + mbConnector *messagebrokerfixture.TestConnector, + lifeCycleHooks ...*connectorsfixture.ConnectorLifecycleHooks, +) { + *dbConnector = newDBConnector() + *mbConnector = newMBConnector() + + var connectorLifecycleHooks *connectorsfixture.ConnectorLifecycleHooks + if len(lifeCycleHooks) == 1 { + connectorLifecycleHooks = lifeCycleHooks[0] + } + + connectorsfixture. + NewTestRunner(applicationYAMLKey, connectorLifecycleHooks, *dbConnector, *mbConnector). + RunTests(m) +} + +func WTestConnectors() []connectorsfixture.TestConnector { + return []connectorsfixture.TestConnector{newDBConnector(), newMBConnector()} +} + +func RTestConnectors() []connectorsfixture.TestConnector { + return []connectorsfixture.TestConnector{newDBConnector()} +} + +func newDBConnector() storagefixture.TestConnector { + return storagefixture.NewTestConnector(applicationYAMLKey, TestConnectorsOrder) +} + +func newMBConnector() messagebrokerfixture.TestConnector { + return messagebrokerfixture.NewTestConnector(applicationYAMLKey, TestConnectorsOrder) +} + +func RContainerMounts() []func(projectRoot string) testcontainers.ContainerMount { + return nil +} + +func WContainerMounts() []func(projectRoot string) testcontainers.ContainerMount { + return nil +} diff --git a/tokenomics/global.go b/tokenomics/global.go new file mode 100644 index 0000000..0edf2f8 --- /dev/null +++ b/tokenomics/global.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + + "github.com/goccy/go-json" + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/go-tarantool-client" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" +) + +func (r *repository) getGlobalUnsignedValue(ctx context.Context, key string) (uint64, error) { + if ctx.Err() != nil { + return 0, errors.Wrap(ctx.Err(), "context failed") + } + var val users.GlobalUnsigned + if err := r.db.GetTyped("GLOBAL", "pk_unnamed_GLOBAL_1", tarantool.StringKey{S: key}, &val); err != nil { + return 0, errors.Wrapf(err, "failed to get global value for key:%v ", key) + } + if val.Key == "" { + return 0, storage.ErrNotFound + } + + return val.Value, nil +} + +func (s *globalTableSource) Process(ctx context.Context, msg *messagebroker.Message) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline while processing message") + } + if len(msg.Value) == 0 { + return nil + } + var val users.GlobalUnsigned + if err := json.UnmarshalContext(ctx, msg.Value, &val); err != nil { + return errors.Wrapf(err, "process: cannot unmarshall %v into %#v", string(msg.Value), &val) + } + if val.Key == "" { + return nil + } + ops := make([]tarantool.Op, 0, 1) + ops = append(ops, tarantool.Op{Op: "=", Field: 1, Arg: val.Value}) + + return errors.Wrapf(s.db.UpsertTyped("GLOBAL", &val, ops, &[]*users.GlobalUnsigned{}), + "failed to upsert global unsigned value:%#v", &val) +} diff --git a/tokenomics/mining.go b/tokenomics/mining.go new file mode 100644 index 0000000..a4d7546 --- /dev/null +++ b/tokenomics/mining.go @@ -0,0 +1,598 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + "strings" + stdlibtime "time" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/wintr/coin" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) GetRankingSummary(ctx context.Context, userID string) (*RankingSummary, error) { //nolint:funlen // A lot of SQL. + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(` +SELECT count(others.user_id) + 1 AS global_rank +FROM (SELECT x.amount_w0, + x.amount_w1, + x.amount_w2, + x.amount_w3 + FROM (SELECT amount_w0, + amount_w1, + amount_w2, + amount_w3 + FROM balances + WHERE user_id = :user_id + UNION ALL + SELECT %[1]v AS amount_w0, + 0 AS amount_w1, + 0 AS amount_w2, + 0 AS amount_w3 + ) AS x + LIMIT 1) AS this + JOIN balances AS others + ON ( + CASE + WHEN others.amount_w3 == this.amount_w3 + THEN ( + CASE + WHEN others.amount_w2 == this.amount_w2 + THEN ( + CASE + WHEN others.amount_w1 == this.amount_w1 + THEN (others.amount_w0 >= this.amount_w0) + ELSE others.amount_w1 > this.amount_w1 + END + ) + ELSE others.amount_w2 > this.amount_w2 + END + ) + ELSE others.amount_w3 > this.amount_w3 + END + ) + AND others.user_id != :user_id +UNION ALL +SELECT (CASE WHEN hide_ranking == TRUE THEN 1 ELSE 2 END) +FROM users +WHERE user_id = :user_id`, registrationICEFlakeBonusAmount) + params := make(map[string]any, 1) + params["user_id"] = userID + resp := make([]*RankingSummary, 0, 1+1) + if err := r.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return nil, errors.Wrapf(err, "failed to select miner global rank for userID:%v", userID) + } + if len(resp) == 1 { + return nil, storage.ErrRelationNotFound + } + if resp[1].GlobalRank == 1 && userID != requestingUserID(ctx) { + return nil, ErrGlobalRankHidden + } + + return resp[0], nil +} + +func (r *repository) GetTopMiners(ctx context.Context, keyword string, limit, offset uint64) ([]*Miner, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + if keyword == "" { + return r.getTopMiners(ctx, limit, offset) + } else { //nolint:revive // Nope. + return r.getTopMinersByKeyword(ctx, keyword, limit, offset) + } +} + +func (r *repository) getTopMinersByKeyword(ctx context.Context, keyword string, limit, offset uint64) ([]*Miner, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(`SELECT b.amount AS amount, + (CASE WHEN u.hide_ranking == TRUE THEN '' ELSE u.user_id END) AS user_id, + (CASE WHEN u.hide_ranking == TRUE THEN '' ELSE u.username END) AS username, + (CASE WHEN u.hide_ranking == TRUE THEN '' ELSE %[2]v END) AS profile_picture_url + FROM users u + JOIN balances b + ON u.user_id = b.user_id + WHERE ( + ( u.username IS NOT NULL AND u.username LIKE :keyword ESCAPE '\' ) + OR + ( u.first_name IS NOT NULL AND u.first_name != '' AND LOWER(u.first_name) LIKE :keyword ESCAPE '\' ) + OR + ( u.last_name IS NOT NULL AND u.last_name != '' AND LOWER(u.last_name) LIKE :keyword ESCAPE '\' ) + ) + ORDER BY b.amount_w3 DESC, + b.amount_w2 DESC, + b.amount_w1 DESC, + b.amount_w0 DESC + LIMIT %[1]v OFFSET :offset`, limit, r.pictureClient.SQLAliasDownloadURL("u.profile_picture_name")) + params := make(map[string]any, 1+1) + params["offset"] = offset + params["keyword"] = fmt.Sprintf("%v%%", strings.ReplaceAll(strings.ReplaceAll(strings.ToLower(keyword), "_", "\\_"), "%", "\\%")) + resp := make([]*Miner, 0, limit) + err := errors.Wrapf(r.db.PrepareExecuteTyped(sql, params, &resp), "failed to select for top miners for params:%#v", params) + + return resp, err +} + +func (r *repository) getTopMiners(ctx context.Context, limit, offset uint64) ([]*Miner, error) { //nolint:revive // . + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(`SELECT b.amount AS amount, + (CASE WHEN u.hide_ranking == TRUE THEN '' ELSE u.user_id END) AS user_id, + (CASE WHEN u.hide_ranking == TRUE THEN '' ELSE u.username END) AS username, + (CASE WHEN u.hide_ranking == TRUE THEN '' ELSE %[2]v END) AS profile_picture_url + FROM balances b + JOIN users u + ON u.user_id = b.user_id + ORDER BY b.amount_w3 DESC, + b.amount_w2 DESC, + b.amount_w1 DESC, + b.amount_w0 DESC + LIMIT %[1]v OFFSET :offset`, limit, r.pictureClient.SQLAliasDownloadURL("u.profile_picture_name")) + params := make(map[string]any, 1) + params["offset"] = offset + resp := make([]*Miner, 0, limit) + if err := r.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return nil, errors.Wrapf(err, "failed to select for top miners for limit:%v,offset:%v", limit, offset) + } + + return resp, nil +} + +//nolint:funlen,lll // . +func (r *repository) GetMiningSummary(ctx context.Context, userID string) (*MiningSummary, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + now := time.Now() + sql := fmt.Sprintf(` +SELECT u.last_natural_mining_started_at, + u.last_mining_started_at, + u.last_mining_ended_at, + current_adoption.base_mining_rate, + btotal.amount AS btotal_amount, + bt0.amount AS bt0_amount, + bt1.amount AS bt1_amount, + bt2.amount AS bt2_amount, + aggressive_degradation_btotal.amount AS aggressive_degradation_btotal_amount, + aggressive_degradation_bt0.amount AS aggressive_degradation_bt0_amount, + aggressive_degradation_bt1.amount AS aggressive_degradation_bt1_amount, + aggressive_degradation_bt2.amount AS aggressive_degradation_bt2_amount, + degradation_btotalt0t1t2.amount AS degradation_btotalt0t1t2_amount, + u.user_id, + (CASE WHEN t0.user_id IS NULL THEN 0 ELSE 1 END) AS t0, + x.t1, + x.t2, + (CASE WHEN IFNULL(eb_worker.extra_bonus_ended_at, 0) > :now_nanos THEN eb_worker.extra_bonus ELSE 0 END) AS current_extra_bonus, + x.pre_staking_allocation, + st_b.bonus, + eb.bonus AS flat_extra_bonus, + (CASE WHEN ((:now_nanos - IFNULL(eb_worker.extra_bonus_started_at, 0) > :claim_window) AND ebw.extra_bonus_index IS NOT NULL) + THEN (100 - (25 * ((CASE WHEN (:now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (ebw.extra_bonus_index * :duration)) - :time_to_availability_window - ((ebw.offset * :availability_window) / :worker_count)) < :first_delayed_claim_penalty_window THEN 0 ELSE (:now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (ebw.extra_bonus_index * :duration)) - :time_to_availability_window - ((ebw.offset * :availability_window) / :worker_count)) END)/:delayed_claim_penalty_window))) + ELSE 0 + END) AS available_flat_extra_bonus_percentage_remaining, + eb_worker.news_seen +FROM (SELECT MAX(st.years) AS pre_staking_years, + MAX(st.allocation) AS pre_staking_allocation, + x.t1, + x.t2, + x.user_id + FROM (SELECT COUNT(t1.user_id) AS t1, + x.t2 AS t2, + x.user_id + FROM (SELECT COUNT(t2.user_id) AS t2, + x.user_id + FROM ( SELECT CAST(:user_id AS STRING) AS user_id ) x + LEFT JOIN users t1_mining_not_required + ON t1_mining_not_required.referred_by = x.user_id + AND t1_mining_not_required.user_id != x.user_id + LEFT JOIN users t2 + ON t2.referred_by = t1_mining_not_required.user_id + AND t2.user_id != t1_mining_not_required.user_id + AND t2.user_id != x.user_id + AND t2.last_mining_ended_at IS NOT NULL + AND t2.last_mining_ended_at > :now_nanos + GROUP BY x.user_id + ) x + LEFT JOIN users t1 + ON t1.referred_by = x.user_id + AND t1.user_id != x.user_id + AND t1.last_mining_ended_at IS NOT NULL + AND t1.last_mining_ended_at > :now_nanos + GROUP BY x.user_id + ) x + LEFT JOIN pre_stakings_%[1]v st + ON st.user_id = x.user_id + GROUP BY x.user_id + ) x + JOIN (%[2]v) current_adoption + JOIN users u + ON u.user_id = x.user_id + JOIN extra_bonus_start_date sd + ON sd.key = 0 + JOIN extra_bonus_processing_worker_%[1]v eb_worker + ON eb_worker.user_id = x.user_id + LEFT JOIN extra_bonuses eb + ON eb.ix = (:now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - sd.value) / :duration + AND :now_nanos + (eb_worker.utc_offset * :utc_offset_duration) > sd.value + AND eb.bonus > 0 + LEFT JOIN extra_bonuses_%[1]v ebw + ON eb.ix = ebw.extra_bonus_index + AND :now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (ebw.extra_bonus_index * :duration)) - :time_to_availability_window - ((ebw.offset * :availability_window) / :worker_count) < :claim_window + AND :now_nanos + (eb_worker.utc_offset * :utc_offset_duration) - (sd.value + (ebw.extra_bonus_index * :duration)) - :time_to_availability_window - ((ebw.offset * :availability_window) / :worker_count) > 0 + LEFT JOIN pre_staking_bonuses st_b + ON st_b.years = x.pre_staking_years + LEFT JOIN balances_%[1]v btotal + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND btotal.user_id = u.user_id + AND btotal.negative = FALSE + AND btotal.type = %[3]v + AND btotal.type_detail = '' + LEFT JOIN balances_%[1]v bt0 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND bt0.user_id = u.user_id + AND bt0.negative = FALSE + AND bt0.type = %[3]v + AND bt0.type_detail = '%[4]v_' || u.referred_by + LEFT JOIN balances_%[1]v bt1 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND bt1.user_id = u.user_id + AND bt1.negative = FALSE + AND bt1.type = %[3]v + AND bt1.type_detail = '%[5]v' + LEFT JOIN balances_%[1]v bt2 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND bt2.user_id = u.user_id + AND bt2.negative = FALSE + AND bt2.type = %[3]v + AND bt2.type_detail = '%[6]v' + LEFT JOIN balances_%[1]v aggressive_degradation_btotal + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND aggressive_degradation_btotal.user_id = u.user_id + AND aggressive_degradation_btotal.negative = FALSE + AND aggressive_degradation_btotal.type = %[3]v + AND aggressive_degradation_btotal.type_detail = '%[7]v' + LEFT JOIN balances_%[1]v aggressive_degradation_bt0 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND aggressive_degradation_bt0.user_id = u.user_id + AND aggressive_degradation_bt0.negative = FALSE + AND aggressive_degradation_bt0.type = %[3]v + AND aggressive_degradation_bt0.type_detail = '%[4]v_' || u.referred_by || '_' + LEFT JOIN balances_%[1]v aggressive_degradation_bt1 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND aggressive_degradation_bt1.user_id = u.user_id + AND aggressive_degradation_bt1.negative = FALSE + AND aggressive_degradation_bt1.type = %[3]v + AND aggressive_degradation_bt1.type_detail = '%[8]v' + LEFT JOIN balances_%[1]v aggressive_degradation_bt2 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND aggressive_degradation_bt2.user_id = u.user_id + AND aggressive_degradation_bt2.negative = FALSE + AND aggressive_degradation_bt2.type = %[3]v + AND aggressive_degradation_bt2.type_detail = '%[9]v' + LEFT JOIN balances_%[1]v degradation_btotalt0t1t2 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND degradation_btotalt0t1t2.user_id = u.user_id + AND degradation_btotalt0t1t2.negative = FALSE + AND degradation_btotalt0t1t2.type = %[3]v + AND degradation_btotalt0t1t2.type_detail = '%[10]v' + LEFT JOIN users t0 + ON t0.user_id = u.referred_by + AND t0.user_id != x.user_id + AND t0.last_mining_ended_at IS NOT NULL + AND t0.last_mining_ended_at > :now_nanos`, + r.workerIndex(ctx), + currentAdoptionSQL(), + totalNoPreStakingBonusBalanceType, + t0BalanceTypeDetail, + t1BalanceTypeDetail, + t2BalanceTypeDetail, + aggressiveDegradationTotalReferenceBalanceTypeDetail, + aggressiveDegradationT1ReferenceBalanceTypeDetail, + aggressiveDegradationT2ReferenceBalanceTypeDetail, + degradationT0T1T2TotalReferenceBalanceTypeDetail, + ) + const networkLagDelta = 1.3 + params := make(map[string]any, 10) //nolint:gomnd // . + params["user_id"] = userID + params["now_nanos"] = now + params["duration"] = r.cfg.ExtraBonuses.Duration + params["utc_offset_duration"] = r.cfg.ExtraBonuses.UTCOffsetDuration + params["availability_window"] = r.cfg.ExtraBonuses.AvailabilityWindow + params["delayed_claim_penalty_window"] = r.cfg.ExtraBonuses.DelayedClaimPenaltyWindow + params["first_delayed_claim_penalty_window"] = stdlibtime.Duration(float64(r.cfg.ExtraBonuses.DelayedClaimPenaltyWindow.Nanoseconds()) * networkLagDelta) + params["time_to_availability_window"] = r.cfg.ExtraBonuses.TimeToAvailabilityWindow + params["claim_window"] = r.cfg.ExtraBonuses.ClaimWindow + params["worker_count"] = r.cfg.WorkerCount + resp := make([]*struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:tagliatelle,revive,nosnakecase // To insert we need asArray + LastNaturalMiningStartedAt *time.Time + LastMiningStartedAt *time.Time + LastMiningEndedAt *time.Time + BaseMiningRate *coin.ICEFlake + TotalAmount *coin.ICEFlake + T0Amount *coin.ICEFlake + T1Amount *coin.ICEFlake + T2Amount *coin.ICEFlake + AggressiveDegradationReferenceTotalAmount *coin.ICEFlake + AggressiveDegradationReferenceT0Amount *coin.ICEFlake + AggressiveDegradationReferenceT1Amount *coin.ICEFlake + AggressiveDegradationReferenceT2Amount *coin.ICEFlake + DegradationReferenceTotalT0T1T2Amount *coin.ICEFlake + userMiningRateRecalculationParameters + FlatBonus uint64 + BonusPercentageRemaining uint64 + NewsSeen uint64 + }, 0, 1) + if err := r.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return nil, errors.Wrapf(err, "failed to select for mining summary for userID:%v", userID) + } + if len(resp) == 0 { + return nil, storage.ErrRelationNotFound + } + var mrt MiningRateType + var negativeMiningRate *coin.ICEFlake + if resp[0].LastMiningEndedAt == nil { //nolint:gocritic,nestif // Wrong. + mrt = NoneMiningRateType + } else if resp[0].LastMiningEndedAt.After(*now.Time) { + mrt = PositiveMiningRateType + } else if resp[0].TotalAmount.Add(resp[0].T0Amount).Add(resp[0].T1Amount).Add(resp[0].T2Amount).IsZero() { + mrt = NoneMiningRateType + } else { + mrt = NegativeMiningRateType + if aggressive := resp[0].LastMiningEndedAt.Add(r.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time); aggressive { + referenceAmount := resp[0].AggressiveDegradationReferenceTotalAmount. + Add(resp[0].AggressiveDegradationReferenceT0Amount). + Add(resp[0].AggressiveDegradationReferenceT1Amount). + Add(resp[0].AggressiveDegradationReferenceT2Amount) + negativeMiningRate = r.calculateDegradation(r.cfg.GlobalAggregationInterval.Child, referenceAmount, true) + } else { + negativeMiningRate = r.calculateDegradation(r.cfg.GlobalAggregationInterval.Child, resp[0].DegradationReferenceTotalT0T1T2Amount, false) + } + } + miningStreak := r.calculateMiningStreak(now, resp[0].LastMiningStartedAt, resp[0].LastMiningEndedAt) + + return &MiningSummary{ + MiningStreak: miningStreak, + MiningSession: r.calculateMiningSession(now, resp[0].LastNaturalMiningStartedAt, resp[0].LastMiningEndedAt), + RemainingFreeMiningSessions: r.calculateRemainingFreeMiningSessions(now, resp[0].LastMiningEndedAt), + MiningRates: r.calculateMiningRateSummaries(resp[0].BaseMiningRate, &resp[0].userMiningRateRecalculationParameters, negativeMiningRate, mrt), //nolint:lll // . + ExtraBonusSummary: ExtraBonusSummary{ + AvailableExtraBonus: r.calculateExtraBonus(resp[0].FlatBonus, resp[0].BonusPercentageRemaining, resp[0].NewsSeen, miningStreak), + }, + }, nil +} + +func (r *repository) calculateMiningSession(now, start, end *time.Time) (ms *MiningSession) { + if end == nil || end.Before(*now.Time) { + return nil + } + lastMiningStartedAt := time.New(start.Add((now.Sub(*start.Time) / r.cfg.MiningSessionDuration.Max) * r.cfg.MiningSessionDuration.Max)) + free := start.Add(r.cfg.MiningSessionDuration.Max).Before(*now.Time) + + return &MiningSession{ + StartedAt: lastMiningStartedAt, + EndedAt: time.New(lastMiningStartedAt.Add(r.cfg.MiningSessionDuration.Max)), + Free: &free, + ResettableStartingAt: time.New(lastMiningStartedAt.Add(r.cfg.MiningSessionDuration.Min)), + WarnAboutExpirationStartingAt: time.New(lastMiningStartedAt.Add(r.cfg.MiningSessionDuration.WarnAboutExpirationAfter)), + } +} + +func (r *repository) calculateMiningRateSummaries( //nolint:funlen // A lot of calculations. + baseMiningRate *coin.ICEFlake, params *userMiningRateRecalculationParameters, negativeMiningRate *coin.ICEFlake, miningRateType MiningRateType, +) (miningRates *MiningRates[MiningRateSummary[coin.ICE]]) { + miningRates = new(MiningRates[MiningRateSummary[coin.ICE]]) + miningRates.Type = miningRateType + var ( + standardMiningRate, preStakingMiningRate *coin.ICEFlake + preStakingBonusVal uint64 + ) + miningRates.Base = &MiningRateSummary[coin.ICE]{ + Amount: baseMiningRate.UnsafeICE(), + } + if params.PreStakingAllocation != percentage100 { + var totalBonus uint64 + switch miningRates.Type { + case PositiveMiningRateType: + standardMiningRate = r.calculateMintedStandardCoins(baseMiningRate, params, r.cfg.GlobalAggregationInterval.Child, false) + totalBonus = coin.ZeroICEFlakes(). + Add(standardMiningRate). + Subtract(baseMiningRate). + MultiplyUint64(percentage100). + Divide(baseMiningRate).Uint64() + case NegativeMiningRateType: + standardMiningRate = coin.ZeroICEFlakes(). + Add(negativeMiningRate. + MultiplyUint64(percentage100 - params.PreStakingAllocation). + DivideUint64(percentage100)) + case NoneMiningRateType: + standardMiningRate = coin.ZeroICEFlakes() + } + miningRates.Standard = &MiningRateSummary[coin.ICE]{ + Amount: standardMiningRate.UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: uint64(float64((params.T0*r.cfg.ReferralBonusMiningRates.T0+params.T1*r.cfg.ReferralBonusMiningRates.T1)*(percentage100-params.PreStakingAllocation)) / float64(percentage100)), //nolint:lll // . + T2: uint64(float64(params.T2*r.cfg.ReferralBonusMiningRates.T2*(percentage100-params.PreStakingAllocation)) / float64(percentage100)), + Extra: uint64(float64(params.ExtraBonus*(percentage100-params.PreStakingAllocation)) / float64(percentage100)), + Total: totalBonus, + }, + } + } + if params.PreStakingAllocation != 0 { + var totalBonus uint64 + switch miningRates.Type { + case PositiveMiningRateType: + preStakingMiningRate = r.calculateMintedPreStakingCoins(baseMiningRate, params, r.cfg.GlobalAggregationInterval.Child, false) + totalBonus = coin.ZeroICEFlakes(). + Add(preStakingMiningRate). + Subtract(baseMiningRate). + MultiplyUint64(percentage100). + Divide(baseMiningRate).Uint64() + case NegativeMiningRateType: + preStakingMiningRate = coin.ZeroICEFlakes(). + Add(negativeMiningRate. + MultiplyUint64((params.PreStakingBonus + percentage100) * params.PreStakingAllocation). + DivideUint64(percentage100 * percentage100)) + case NoneMiningRateType: + preStakingMiningRate = coin.ZeroICEFlakes() + } + t1Bonus := float64((params.T0*r.cfg.ReferralBonusMiningRates.T0+params.T1*r.cfg.ReferralBonusMiningRates.T1)*params.PreStakingAllocation) / float64(percentage100) //nolint:lll // . + t2Bonus := float64(params.T2*r.cfg.ReferralBonusMiningRates.T2*params.PreStakingAllocation) / float64(percentage100) + extraBonus := float64(params.ExtraBonus*params.PreStakingAllocation) / float64(percentage100) + preStakingBonusVal = uint64((float64(params.PreStakingAllocation) * float64(params.PreStakingBonus)) / float64(percentage100)) + miningRates.PreStaking = &MiningRateSummary[coin.ICE]{ + Amount: preStakingMiningRate.UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: uint64(t1Bonus), + T2: uint64(t2Bonus), + Extra: uint64(extraBonus), + PreStaking: preStakingBonusVal, + Total: totalBonus, + }, + } + } + totalNoStakingBonusParams := *params + totalNoStakingBonusParams.PreStakingAllocation, totalNoStakingBonusParams.PreStakingBonus = 0, 0 + positiveTotalNoPreStakingBonus := r.calculateMintedStandardCoins(baseMiningRate, &totalNoStakingBonusParams, r.cfg.GlobalAggregationInterval.Child, false) + positiveTotalNoPreStakingBonusVal := positiveTotalNoPreStakingBonus. + Subtract(baseMiningRate). + MultiplyUint64(percentage100). + Divide(baseMiningRate).Uint64() + var totalBonus, totalNoPreStakingBonusVal uint64 + var totalNoPreStakingBonus *coin.ICEFlake + switch miningRates.Type { + case PositiveMiningRateType: + totalBonus = coin.ZeroICEFlakes(). + Add(standardMiningRate). + Add(preStakingMiningRate). + Subtract(baseMiningRate). + MultiplyUint64(percentage100). + Divide(baseMiningRate).Uint64() + totalNoPreStakingBonus = positiveTotalNoPreStakingBonus + totalNoPreStakingBonusVal = positiveTotalNoPreStakingBonusVal + case NegativeMiningRateType: + totalNoPreStakingBonus = coin.ZeroICEFlakes().Add(negativeMiningRate) + case NoneMiningRateType: + totalNoPreStakingBonus = coin.ZeroICEFlakes() + } + miningRates.Total = &MiningRateSummary[coin.ICE]{ + Amount: standardMiningRate.Add(preStakingMiningRate).UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: params.T0*r.cfg.ReferralBonusMiningRates.T0 + params.T1*r.cfg.ReferralBonusMiningRates.T1, + T2: params.T2 * r.cfg.ReferralBonusMiningRates.T2, + Extra: params.ExtraBonus, + PreStaking: preStakingBonusVal, + Total: totalBonus, + }, + } + miningRates.TotalNoPreStakingBonus = &MiningRateSummary[coin.ICE]{ + Amount: totalNoPreStakingBonus.UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: params.T0*r.cfg.ReferralBonusMiningRates.T0 + params.T1*r.cfg.ReferralBonusMiningRates.T1, + T2: params.T2 * r.cfg.ReferralBonusMiningRates.T2, + Extra: params.ExtraBonus, + PreStaking: 0, + Total: totalNoPreStakingBonusVal, + }, + } + miningRates.PositiveTotalNoPreStakingBonus = &MiningRateSummary[coin.ICE]{ + Amount: positiveTotalNoPreStakingBonus.UnsafeICE(), + Bonuses: &MiningRateBonuses{ + T1: params.T0*r.cfg.ReferralBonusMiningRates.T0 + params.T1*r.cfg.ReferralBonusMiningRates.T1, + T2: params.T2 * r.cfg.ReferralBonusMiningRates.T2, + Extra: params.ExtraBonus, + PreStaking: 0, + Total: positiveTotalNoPreStakingBonusVal, + }, + } + + return miningRates +} + +func (r *repository) calculateMintedStandardCoins( //nolint:revive // Not an issue here. + baseMiningRate *coin.ICEFlake, params *userMiningRateRecalculationParameters, elapsedNanos stdlibtime.Duration, excludeBaseRate bool, +) *coin.ICEFlake { + if params.PreStakingAllocation == percentage100 || elapsedNanos <= 0 { + return nil + } + var includeBaseMiningRate uint64 + if !excludeBaseRate { + includeBaseMiningRate = percentage100 + params.ExtraBonus + } + mintedBase := includeBaseMiningRate + + params.T0*r.cfg.ReferralBonusMiningRates.T0 + + params.T1*r.cfg.ReferralBonusMiningRates.T1 + + params.T2*r.cfg.ReferralBonusMiningRates.T2 + if mintedBase == 0 { + return nil + } + if elapsedNanos == r.cfg.GlobalAggregationInterval.Child { + return baseMiningRate. + MultiplyUint64(mintedBase). + MultiplyUint64(percentage100 - params.PreStakingAllocation). + DivideUint64(percentage100 * percentage100) + } + + return baseMiningRate. + MultiplyUint64(uint64(elapsedNanos.Nanoseconds())). + MultiplyUint64(mintedBase). + MultiplyUint64(percentage100 - params.PreStakingAllocation). + DivideUint64(uint64(r.cfg.GlobalAggregationInterval.Child.Nanoseconds()) * percentage100 * percentage100) +} + +func (r *repository) calculateMintedPreStakingCoins( //nolint:revive // Not an issue here. + baseMiningRate *coin.ICEFlake, params *userMiningRateRecalculationParameters, elapsedNanos stdlibtime.Duration, excludeBaseRate bool, +) *coin.ICEFlake { + if params.PreStakingAllocation == 0 || elapsedNanos <= 0 { + return nil + } + var includeBaseMiningRate uint64 + if !excludeBaseRate { + includeBaseMiningRate = percentage100 + params.ExtraBonus + } + mintedBase := includeBaseMiningRate + + params.T0*r.cfg.ReferralBonusMiningRates.T0 + + params.T1*r.cfg.ReferralBonusMiningRates.T1 + + params.T2*r.cfg.ReferralBonusMiningRates.T2 + if mintedBase == 0 { + return nil + } + if elapsedNanos == r.cfg.GlobalAggregationInterval.Child { + return baseMiningRate. + MultiplyUint64(mintedBase). + MultiplyUint64((params.PreStakingBonus + percentage100) * params.PreStakingAllocation). + DivideUint64(percentage100 * percentage100 * percentage100) + } + + return baseMiningRate. + MultiplyUint64(uint64(elapsedNanos.Nanoseconds())). + MultiplyUint64(mintedBase). + MultiplyUint64((params.PreStakingBonus + percentage100) * params.PreStakingAllocation). + DivideUint64(uint64(r.cfg.GlobalAggregationInterval.Child.Nanoseconds()) * percentage100 * percentage100 * percentage100) +} + +func (r *repository) calculateMiningStreak(now, start, end *time.Time) uint64 { + if start == nil || end == nil || now.After(*end.Time) || now.Before(*start.Time) { + return 0 + } + + return uint64(now.Sub(*start.Time) / r.cfg.MiningSessionDuration.Max) +} + +func (r *repository) calculateRemainingFreeMiningSessions(now, end *time.Time) uint64 { + if end == nil || now.After(*end.Time) { + return 0 + } + + return uint64(end.Sub(*now.Time) / r.cfg.MiningSessionDuration.Max) +} diff --git a/tokenomics/mining_rates_recalculation.go b/tokenomics/mining_rates_recalculation.go new file mode 100644 index 0000000..bd7fc44 --- /dev/null +++ b/tokenomics/mining_rates_recalculation.go @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + stdlibtime "time" + + "github.com/cenkalti/backoff/v4" + "github.com/goccy/go-json" + "github.com/google/uuid" + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/wintr/coin" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) initializeMiningRatesRecalculationWorker(ctx context.Context, usr *users.User) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + workerIndex := usr.HashCode % r.cfg.WorkerCount + err := retry(ctx, func() error { + if err := r.initializeWorker(ctx, "mining_rates_recalculation_worker_", usr.ID, workerIndex); err != nil { + if errors.Is(err, storage.ErrRelationNotFound) { + return err + } + + return errors.Wrapf(backoff.Permanent(err), "failed to initializeMiningRatesRecalculationWorker for userID:%v,workerIndex:%v", usr.ID, workerIndex) + } + + return nil + }) + + return errors.Wrapf(err, "permanently failed to initializeMiningRatesRecalculationWorker for userID:%v,workerIndex:%v", usr.ID, workerIndex) +} + +func (p *processor) startMiningRatesRecalculationTriggerSeedingStream(ctx context.Context) { + nilBodyForEachWorker := make([]any, p.cfg.WorkerCount) //nolint:makezero // Intended. + ticker := stdlibtime.NewTicker(refreshMiningRatesProcessingSeedingStreamEmitFrequency) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + log.Error(errors.Wrap(sendMessagesConcurrently[any](ctx, p.sendMiningRatesRecalculationTriggerMessage, nilBodyForEachWorker), + "failed to sendMessagesConcurrently[sendMiningRatesRecalculationTriggerMessage]")) + case <-ctx.Done(): + return + } + } +} + +func (p *processor) sendMiningRatesRecalculationTriggerMessage(ctx context.Context, _ any) error { + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: uuid.NewString(), + Topic: p.cfg.MessageBroker.Topics[10].Name, + } + responder := make(chan error, 1) + defer close(responder) + p.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send `%v` message to broker", msg.Topic) +} + +func (s *miningRatesRecalculationTriggerStreamSource) Process(ignoredCtx context.Context, msg *messagebroker.Message) (err error) { + if ignoredCtx.Err() != nil { + return errors.Wrap(ignoredCtx.Err(), "unexpected deadline while processing message") + } + const deadline = 5 * stdlibtime.Minute + ctx, cancel := context.WithTimeout(context.Background(), deadline) + defer cancel() + now := time.Now() + rows, err := s.getLatestMiningRates(ctx, uint64(msg.Partition), now) //nolint:contextcheck // We use context with longer deadline. + if err != nil || len(rows) == 0 { + return errors.Wrapf(err, "failed to getLatestMiningRates for workerIndex:%v", msg.Partition) + } + if err = sendMessagesConcurrently(ctx, s.sendMiningRatesMessage, rows); err != nil { //nolint:contextcheck // We use context with longer deadline. + return errors.Wrapf(err, "failed to sendMiningRatesMessages for:%#v", rows) + } + + return errors.Wrapf(s.updateLastIterationFinishedAt(ctx, uint64(msg.Partition), rows, now), //nolint:contextcheck // We use context with longer deadline. + "failed to updateLastIterationFinishedAt for workerIndex:%v,rows:%#v", msg.Partition, rows) +} + +func (s *miningRatesRecalculationTriggerStreamSource) getLatestMiningRates( //nolint:funlen,gocognit // . + ctx context.Context, workerIndex uint64, now *time.Time, +) ([]*MiningRates[coin.ICEFlake], error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + currentAdoption, err := s.getCurrentAdoption(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to getCurrentAdoption") + } + batch, err := s.getUserMiningRateCalculationParametersNewBatch(ctx, workerIndex, now) + if err != nil { + return nil, errors.Wrapf(err, "failed to getUserMiningRateCalculationParametersBatch for workerIndex:%v", workerIndex) + } + if len(batch) == 0 { + return nil, nil + } + res := make([]*MiningRates[coin.ICEFlake], 0, len(batch)) + for _, row := range batch { + var negativeMiningRate *coin.ICEFlake + if row.LastMiningEndedAt != nil && row.LastMiningEndedAt.Before(*now.Time) { + if aggressive := row.LastMiningEndedAt.Add(s.cfg.RollbackNegativeMining.AggressiveDegradationStartsAfter).Before(*now.Time); aggressive { + referenceAmount := row.AggressiveDegradationReferenceTotalAmount. + Add(row.AggressiveDegradationReferenceT0Amount). + Add(row.AggressiveDegradationReferenceT1Amount). + Add(row.AggressiveDegradationReferenceT2Amount) + negativeMiningRate = s.calculateDegradation(s.cfg.GlobalAggregationInterval.Child, referenceAmount, true) + } else { + negativeMiningRate = s.calculateDegradation(s.cfg.GlobalAggregationInterval.Child, row.DegradationReferenceTotalT1T2Amount, false) + } + if negativeMiningRate.IsNil() { + negativeMiningRate = coin.ZeroICEFlakes() + } + } + res = append(res, s.calculateICEFlakeMiningRates(currentAdoption.BaseMiningRate, row, negativeMiningRate)) + } + + return res, nil +} + +type ( + latestMiningRateCalculationSQLRow struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + LastMiningEndedAt *time.Time + AggressiveDegradationReferenceTotalAmount *coin.ICEFlake + AggressiveDegradationReferenceT0Amount *coin.ICEFlake + AggressiveDegradationReferenceT1Amount *coin.ICEFlake + AggressiveDegradationReferenceT2Amount *coin.ICEFlake + DegradationReferenceTotalT1T2Amount *coin.ICEFlake + userMiningRateRecalculationParameters + } +) + +func (s *miningRatesRecalculationTriggerStreamSource) getUserMiningRateCalculationParametersNewBatch( //nolint:funlen,gocritic// . + ctx context.Context, workerIndex uint64, now *time.Time, +) ([]*latestMiningRateCalculationSQLRow, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(` +SELECT u.last_mining_ended_at, + aggressive_degradation_btotal.amount AS aggressive_degradation_btotal_amount, + aggressive_degradation_bt0.amount AS aggressive_degradation_bt0_amount, + aggressive_degradation_bt1.amount AS aggressive_degradation_bt1_amount, + aggressive_degradation_bt2.amount AS aggressive_degradation_bt2_amount, + degradation_btotalt0t1t2.amount AS degradation_btotalt0t1t2_amount, + u.user_id, + (CASE WHEN t0.user_id IS NULL THEN 0 ELSE 1 END) AS t0, + x.t1, + x.t2, + (CASE WHEN IFNULL(eb_worker.extra_bonus_ended_at, 0) > :now_nanos THEN eb_worker.extra_bonus ELSE 0 END) AS extra_bonus, + x.pre_staking_allocation, + st_b.bonus +FROM (SELECT MAX(st.years) AS pre_staking_years, + MAX(st.allocation) AS pre_staking_allocation, + x.t1, + x.t2, + x.user_id + FROM (SELECT COUNT(t1.user_id) AS t1, + x.t2 AS t2, + x.user_id + FROM ( SELECT COUNT(t2.user_id) AS t2, + x.user_id + FROM ( SELECT user_id + FROM mining_rates_recalculation_worker_%[2]v + ORDER BY last_iteration_finished_at + LIMIT %[1]v ) x + LEFT JOIN users t1_mining_not_required + ON t1_mining_not_required.referred_by = x.user_id + AND t1_mining_not_required.user_id != x.user_id + LEFT JOIN users t2 + ON t2.referred_by = t1_mining_not_required.user_id + AND t2.user_id != t1_mining_not_required.user_id + AND t2.user_id != x.user_id + AND t2.last_mining_ended_at IS NOT NULL + AND t2.last_mining_ended_at > :now_nanos + GROUP BY x.user_id + ) x + LEFT JOIN users t1 + ON t1.referred_by = x.user_id + AND t1.user_id != x.user_id + AND t1.last_mining_ended_at IS NOT NULL + AND t1.last_mining_ended_at > :now_nanos + GROUP BY x.user_id + ) x + LEFT JOIN pre_stakings_%[2]v st + ON st.user_id = x.user_id + GROUP BY x.user_id + ) x + JOIN users u + ON u.user_id = x.user_id + JOIN extra_bonus_processing_worker_%[2]v eb_worker + ON eb_worker.user_id = x.user_id + LEFT JOIN pre_staking_bonuses st_b + ON st_b.years = x.pre_staking_years + LEFT JOIN balances_%[2]v aggressive_degradation_btotal + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND aggressive_degradation_btotal.user_id = u.user_id + AND aggressive_degradation_btotal.negative = FALSE + AND aggressive_degradation_btotal.type = %[3]v + AND aggressive_degradation_btotal.type_detail = '%[4]v' + LEFT JOIN balances_%[2]v aggressive_degradation_bt0 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND aggressive_degradation_bt0.user_id = u.user_id + AND aggressive_degradation_bt0.negative = FALSE + AND aggressive_degradation_bt0.type = %[3]v + AND aggressive_degradation_bt0.type_detail = '%[5]v_' || u.referred_by || '_' + LEFT JOIN balances_%[2]v aggressive_degradation_bt1 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND aggressive_degradation_bt1.user_id = u.user_id + AND aggressive_degradation_bt1.negative = FALSE + AND aggressive_degradation_bt1.type = %[3]v + AND aggressive_degradation_bt1.type_detail = '%[6]v' + LEFT JOIN balances_%[2]v aggressive_degradation_bt2 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND aggressive_degradation_bt2.user_id = u.user_id + AND aggressive_degradation_bt2.negative = FALSE + AND aggressive_degradation_bt2.type = %[3]v + AND aggressive_degradation_bt2.type_detail = '%[7]v' + LEFT JOIN balances_%[2]v degradation_btotalt0t1t2 + ON (u.last_mining_ended_at IS NOT NULL AND u.last_mining_ended_at < :now_nanos ) + AND degradation_btotalt0t1t2.user_id = u.user_id + AND degradation_btotalt0t1t2.negative = FALSE + AND degradation_btotalt0t1t2.type = %[3]v + AND degradation_btotalt0t1t2.type_detail = '%[8]v' + LEFT JOIN users t0 + ON t0.user_id = u.referred_by + AND t0.user_id != x.user_id + AND t0.last_mining_ended_at IS NOT NULL + AND t0.last_mining_ended_at > :now_nanos`, + miningRatesRecalculationBatchSize, + workerIndex, + totalNoPreStakingBonusBalanceType, + aggressiveDegradationTotalReferenceBalanceTypeDetail, + t0BalanceTypeDetail, + aggressiveDegradationT1ReferenceBalanceTypeDetail, + aggressiveDegradationT2ReferenceBalanceTypeDetail, + degradationT0T1T2TotalReferenceBalanceTypeDetail) + params := make(map[string]any, 1) + params["now_nanos"] = now + res := make([]*latestMiningRateCalculationSQLRow, 0, miningRatesRecalculationBatchSize) + if err := s.db.PrepareExecuteTyped(sql, params, &res); err != nil { + return nil, errors.Wrapf(err, "failed to select a batch of latest user mining rate calculation parameters for workerIndex:%v", workerIndex) + } + + return res, nil +} + +func (s *miningRatesRecalculationTriggerStreamSource) sendMiningRatesMessage(ctx context.Context, mnrs *MiningRates[coin.ICEFlake]) error { + valueBytes, err := json.MarshalContext(ctx, mnrs) + if err != nil { + return errors.Wrapf(err, "failed to marshal %#v", mnrs) + } + + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: mnrs.UserID, + Topic: s.cfg.MessageBroker.Topics[3].Name, + Value: valueBytes, + } + + responder := make(chan error, 1) + defer close(responder) + s.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send %v message to broker, msg:%#v", msg.Topic, mnrs) +} + +func (r *repository) calculateICEFlakeMiningRates( + baseMiningRate *coin.ICEFlake, row *latestMiningRateCalculationSQLRow, negativeMiningRate *coin.ICEFlake, +) (miningRates *MiningRates[coin.ICEFlake]) { + miningRates = new(MiningRates[coin.ICEFlake]) + + if !negativeMiningRate.IsNil() { + miningRates.Type = NegativeMiningRateType + if row.PreStakingAllocation != percentage100 { + miningRates.Standard = negativeMiningRate. + MultiplyUint64(percentage100 - row.PreStakingAllocation). + DivideUint64(percentage100) + } + if row.PreStakingAllocation != 0 { + miningRates.PreStaking = negativeMiningRate. + MultiplyUint64((row.PreStakingBonus + percentage100) * row.PreStakingAllocation). + DivideUint64(percentage100 * percentage100) + } + + return miningRates + } + + miningRates.Type = PositiveMiningRateType + params := &row.userMiningRateRecalculationParameters + if standard := r.calculateMintedStandardCoins(baseMiningRate, params, r.cfg.GlobalAggregationInterval.Child, false); !standard.IsZero() { + miningRates.Standard = standard + } + if preStaking := r.calculateMintedPreStakingCoins(baseMiningRate, params, r.cfg.GlobalAggregationInterval.Child, false); !preStaking.IsZero() { + miningRates.PreStaking = preStaking + } + + return miningRates +} + +func (s *miningRatesRecalculationTriggerStreamSource) updateLastIterationFinishedAt( + ctx context.Context, workerIndex uint64, rows []*MiningRates[coin.ICEFlake], now *time.Time, +) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + userIDs := make([]string, 0, len(rows)) + for i := range rows { + userIDs = append(userIDs, rows[i].UserID) + } + const table = "mining_rates_recalculation_worker_" + params := make(map[string]any, 1) + params["last_iteration_finished_at"] = now + err := s.updateWorkerFields(ctx, workerIndex, table, params, userIDs...) + + return errors.Wrapf(err, "failed to updateWorkerTimeField for workerIndex:%v,table:%q,params:%#v,userIDs:%#v", workerIndex, table, params, userIDs) +} diff --git a/tokenomics/mining_sessions.go b/tokenomics/mining_sessions.go new file mode 100644 index 0000000..5e38082 --- /dev/null +++ b/tokenomics/mining_sessions.go @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + "strings" + stdlibtime "time" + + "github.com/goccy/go-json" + "github.com/google/uuid" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + + "github.com/ice-blockchain/wintr/coin" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/terror" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) StartNewMiningSession( //nolint:funlen,gocognit // A lot of handling. + ctx context.Context, ms *MiningSummary, rollbackNegativeMiningProgress *bool, +) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + userID := *ms.MiningSession.UserID + old, err := r.getInternalMiningSummary(ctx, userID) + if err != nil { + return errors.Wrapf(err, "failed to getMiningSummary for userID:%v", userID) + } + now := time.Now() + if old.LastMiningEndedAt != nil && + old.LastNaturalMiningStartedAt != nil && + old.LastMiningEndedAt.After(*now.Time) && + (now.Sub(*old.LastNaturalMiningStartedAt.Time)/r.cfg.MiningSessionDuration.Min)%2 == 0 { + return ErrDuplicate + } + shouldRollback, err := r.validateRollbackNegativeMiningProgress(old, now, rollbackNegativeMiningProgress) + if err != nil { + return err + } + newMS := r.newMiningSummary(old, now) + if err = r.insertNewMiningSession(ctx, userID, old, newMS, shouldRollback); err != nil { + return errors.Wrapf(err, + "failed to insertNewMiningSession:%#v,userID:%v,rollbackNegativeMiningProgress:%v", newMS, userID, shouldRollback) + } + if err = retry(ctx, func() error { + summary, gErr := r.GetMiningSummary(ctx, userID) + if gErr == nil { + if summary.MiningSession == nil || summary.MiningSession.StartedAt == nil || !summary.MiningSession.StartedAt.Equal(*now.Time) { + gErr = ErrNotFound + } else { + *ms = *summary + } + } + + return gErr + }); err != nil { + return errors.Wrapf(err, "permanently failed to GetMiningSummary for userID:%v", userID) + } + + return errors.Wrapf(r.trySendMiningSessionMessage(ctx, userID, newMS), + "failed to trySendMiningSessionMessage:%#v,userID:%v", ms, userID) +} + +func (r *repository) getInternalMiningSummary(ctx context.Context, userID string) (*miningSummary, error) { //nolint:funlen // Big SQL. + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(`SELECT u.last_natural_mining_started_at, + u.last_mining_started_at, + u.last_mining_ended_at, + u.previous_mining_started_at, + u.previous_mining_ended_at, + u.last_free_mining_session_awarded_at, + negative_balance.amount, + negative_t0_balance.amount, + negative_t1_balance.amount, + negative_t2_balance.amount, + 0 AS mining_streak, + MAX(st.years) AS years, + MAX(st.allocation) AS allocation, + st_b.bonus + FROM users u + LEFT JOIN pre_stakings_%[1]v st + ON st.user_id = u.user_id + LEFT JOIN pre_staking_bonuses st_b + ON st.years = st_b.years + LEFT JOIN balances_%[1]v negative_balance + ON u.rollback_used_at IS NULL + AND negative_balance.user_id = u.user_id + AND negative_balance.negative = TRUE + AND negative_balance.type = %[2]v + AND negative_balance.type_detail = '' + LEFT JOIN balances_%[1]v negative_t0_balance + ON u.rollback_used_at IS NULL + AND negative_t0_balance.user_id = u.user_id + AND negative_t0_balance.negative = TRUE + AND negative_t0_balance.type = %[2]v + AND negative_t0_balance.type_detail = '%[3]v_' || u.referred_by + LEFT JOIN balances_%[1]v negative_t1_balance + ON u.rollback_used_at IS NULL + AND negative_t1_balance.user_id = u.user_id + AND negative_t1_balance.negative = TRUE + AND negative_t1_balance.type = %[2]v + AND negative_t1_balance.type_detail = '%[4]v' + LEFT JOIN balances_%[1]v negative_t2_balance + ON u.rollback_used_at IS NULL + AND negative_t2_balance.user_id = u.user_id + AND negative_t2_balance.negative = TRUE + AND negative_t2_balance.type = %[2]v + AND negative_t2_balance.type_detail = '%[5]v' + WHERE u.user_id = :user_id + GROUP BY u.user_id`, r.workerIndex(ctx), totalNoPreStakingBonusBalanceType, t0BalanceTypeDetail, t1BalanceTypeDetail, t2BalanceTypeDetail) + params := make(map[string]any, 1) + params["user_id"] = userID + resp := make([]*miningSummary, 0, 1) + if err := r.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return nil, errors.Wrapf(err, "failed to get the current mining summary for userID:%v", userID) + } + if len(resp) == 0 { + return nil, ErrRelationNotFound + } + + return resp[0], nil +} + +func (r *repository) validateRollbackNegativeMiningProgress( + currentMiningSummary *miningSummary, now *time.Time, rollbackNegativeMiningProgress *bool, +) (*bool, error) { + if currentMiningSummary.LastMiningEndedAt == nil { + return nil, nil //nolint:nilnil // Nope. + } + amountLost := currentMiningSummary.calculateAmountLost() + if !amountLost.IsZero() && + (now.Sub(*currentMiningSummary.LastMiningEndedAt.Time) < r.cfg.RollbackNegativeMining.Available.After || + now.Sub(*currentMiningSummary.LastMiningEndedAt.Time) > r.cfg.RollbackNegativeMining.Available.Until) { + amountLost = nil + } + if rollbackNegativeMiningProgress == nil && !amountLost.IsZero() { + return nil, terror.New(ErrNegativeMiningProgressDecisionRequired, map[string]any{ + "amount": amountLost.UnsafeICE(), + "duringTheLastXSeconds": now.Sub(*currentMiningSummary.LastMiningEndedAt.Time).Milliseconds() / 1e3, //nolint:gomnd // To get to seconds. + }) + } else if rollbackNegativeMiningProgress != nil && amountLost.IsZero() { + return nil, nil //nolint:nilnil // Nope. + } + + return rollbackNegativeMiningProgress, nil +} + +func (m *miningSummary) calculateAmountLost() *coin.ICEFlake { + standardAmount := m.NegativeTotalNoPreStakingBonusBalanceAmount. + MultiplyUint64(percentage100 - m.PreStakingAllocation). + DivideUint64(percentage100) + preStakingAmount := m.NegativeTotalNoPreStakingBonusBalanceAmount. + MultiplyUint64(m.PreStakingAllocation * (m.PreStakingBonus + percentage100)). + DivideUint64(percentage100 * percentage100) + standardT0Amount := m.NegativeTotalT0NoPreStakingBonusBalanceAmount. + MultiplyUint64(percentage100 - m.PreStakingAllocation). + DivideUint64(percentage100) + preStakingT0Amount := m.NegativeTotalT0NoPreStakingBonusBalanceAmount. + MultiplyUint64(m.PreStakingAllocation * (m.PreStakingBonus + percentage100)). + DivideUint64(percentage100 * percentage100) + standardT1Amount := m.NegativeTotalT1NoPreStakingBonusBalanceAmount. + MultiplyUint64(percentage100 - m.PreStakingAllocation). + DivideUint64(percentage100) + preStakingT1Amount := m.NegativeTotalT1NoPreStakingBonusBalanceAmount. + MultiplyUint64(m.PreStakingAllocation * (m.PreStakingBonus + percentage100)). + DivideUint64(percentage100 * percentage100) + standardT2Amount := m.NegativeTotalT2NoPreStakingBonusBalanceAmount. + MultiplyUint64(percentage100 - m.PreStakingAllocation). + DivideUint64(percentage100) + preStakingT2Amount := m.NegativeTotalT2NoPreStakingBonusBalanceAmount. + MultiplyUint64(m.PreStakingAllocation * (m.PreStakingBonus + percentage100)). + DivideUint64(percentage100 * percentage100) + + return standardAmount.Add(preStakingAmount). + Add(standardT0Amount).Add(preStakingT0Amount). + Add(standardT1Amount).Add(preStakingT1Amount). + Add(standardT2Amount).Add(preStakingT2Amount) +} + +func (r *repository) newMiningSummary(old *miningSummary, now *time.Time) *miningSummary { + resp := &miningSummary{ + LastMiningStartedAt: now, + LastNaturalMiningStartedAt: now, + LastMiningEndedAt: time.New(now.Add(r.cfg.MiningSessionDuration.Max)), + } + if old.LastMiningEndedAt == nil || old.LastMiningStartedAt == nil || old.LastMiningEndedAt.Before(*now.Time) { + return resp + } + resp.LastMiningStartedAt = old.LastMiningStartedAt + resp.LastFreeMiningSessionAwardedAt = old.LastFreeMiningSessionAwardedAt + resp.MiningStreak = r.calculateMiningStreak(now, resp.LastMiningStartedAt, resp.LastMiningEndedAt) + var durationSinceLastFreeMiningSessionAwarded stdlibtime.Duration + if resp.LastFreeMiningSessionAwardedAt == nil { + durationSinceLastFreeMiningSessionAwarded = now.Sub(*resp.LastMiningStartedAt.Time) + } else { + durationSinceLastFreeMiningSessionAwarded = now.Sub(*resp.LastFreeMiningSessionAwardedAt.Time) + } + freeMiningSession := uint64(0) + minimumDurationForAwardingFreeMiningSession := stdlibtime.Duration(r.cfg.ConsecutiveNaturalMiningSessionsRequiredFor1ExtraFreeArtificialMiningSession.Max) * r.cfg.MiningSessionDuration.Max //nolint:lll // . + if durationSinceLastFreeMiningSessionAwarded >= minimumDurationForAwardingFreeMiningSession { + resp.LastFreeMiningSessionAwardedAt = now + freeMiningSession++ + } + if freeSessions := stdlibtime.Duration(r.calculateRemainingFreeMiningSessions(now, old.LastMiningEndedAt) + freeMiningSession); freeSessions > 0 { + resp.LastMiningEndedAt = time.New(resp.LastMiningEndedAt.Add(freeSessions * r.cfg.MiningSessionDuration.Max)) + } + + return resp +} + +func (r *repository) insertNewMiningSession( //nolint:funlen // Big script. + ctx context.Context, userID string, old, ms *miningSummary, rollbackNegativeMiningSession *bool, +) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + var rollbackUsedAt, rollbackUsedAtCondition string + if rollbackNegativeMiningSession != nil && *rollbackNegativeMiningSession { + rollbackUsedAt = fmt.Sprintf("rollback_used_at = %v,", ms.LastNaturalMiningStartedAt.UnixNano()) + rollbackUsedAtCondition = "AND rollback_used_at IS NULL" + } + const null = "null" + previousMiningEndedAtVal := null + if old.LastMiningEndedAt != nil { + previousMiningEndedAtVal = fmt.Sprint(old.LastMiningEndedAt.UnixNano()) + } + lastFreeMiningSessionAwardedAtVal := null + if ms.LastFreeMiningSessionAwardedAt != nil { + lastFreeMiningSessionAwardedAtVal = fmt.Sprint(ms.LastFreeMiningSessionAwardedAt.UnixNano()) + } + script := fmt.Sprintf(`resp, err = box.execute([[START TRANSACTION;]]) +if err ~= nil then + return err +end +resp, err = box.execute([[ UPDATE users + SET updated_at = %[1]v, + last_natural_mining_started_at = %[1]v, + last_mining_started_at = %[2]v, + last_mining_ended_at = %[3]v, + previous_mining_started_at = (CASE WHEN last_mining_started_at = %[2]v THEN previous_mining_started_at ELSE last_mining_started_at END), + previous_mining_ended_at = (CASE WHEN last_mining_started_at = %[2]v THEN previous_mining_ended_at ELSE last_mining_ended_at END), + %[4]v + last_free_mining_session_awarded_at = %[5]v + WHERE user_id = '%[6]v' + AND IFNULL(last_mining_ended_at,0) = IFNULL(%[7]v,0) + %[8]v;]]) +if err ~= nil then + box.execute([[ROLLBACK;]]) + return err +end +if resp.row_count ~= 1 then + box.execute([[ROLLBACK;]]) + return "race condition" +end +resp, err = box.execute([[ UPDATE balance_recalculation_worker_%[9]v + SET enabled = TRUE, + last_mining_started_at = %[2]v, + last_mining_ended_at = %[3]v + WHERE user_id = '%[6]v';]]) +if err ~= nil then + box.execute([[ROLLBACK;]]) + return err +end +if resp.row_count ~= 1 then + box.execute([[ROLLBACK;]]) + return "missing balance_recalculation_worker entry" +end +resp,err = box.execute([[COMMIT;]]) +if err ~= nil then + box.execute([[ROLLBACK;]]) + return err +end +return ''`, + ms.LastNaturalMiningStartedAt.UnixNano(), + ms.LastMiningStartedAt.UnixNano(), + ms.LastMiningEndedAt.UnixNano(), + rollbackUsedAt, + lastFreeMiningSessionAwardedAtVal, + userID, + previousMiningEndedAtVal, + rollbackUsedAtCondition, + r.workerIndex(ctx)) + resp := make([]string, 0, 1) + if err := r.db.EvalTyped(script, []any{}, &resp); err != nil { + return errors.Wrapf(err, "failed to eval script to insert mining session for %#v", ms) + } else if errMessage := resp[0]; errMessage != "" { + if strings.Contains(errMessage, `race condition`) { + return ErrRaceCondition + } + + return errors.Errorf("insert mining session script returned unexpected error message:`%v`, for %#v", errMessage, ms) + } + + return nil +} + +func (r *repository) trySendMiningSessionMessage(ctx context.Context, userID string, newMS *miningSummary) error { //nolint:funlen // . + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + sess := &MiningSession{ + LastNaturalMiningStartedAt: newMS.LastNaturalMiningStartedAt, + StartedAt: newMS.LastMiningStartedAt, + EndedAt: newMS.LastMiningEndedAt, + UserID: &userID, + MiningStreak: newMS.MiningStreak, + } + if err := r.sendMiningSessionMessage(ctx, sess); err != nil { + valueBytes, mErr := json.MarshalContext(ctx, sess) + if mErr != nil { + return multierror.Append( //nolint:wrapcheck // Not needed. + errors.Wrapf(err, "failed to send a new mining session message: %#v", sess), + errors.Wrapf(mErr, "failed to marshal %#v", sess), + ).ErrorOrNil() + } + type ( + MiningSessionDLQ struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // To insert we need asArray + ID, UserID, Message string + } + ) + dlq := &MiningSessionDLQ{ID: uuid.NewString(), UserID: userID, Message: string(valueBytes)} + + return multierror.Append( //nolint:wrapcheck // Not needed. + errors.Wrapf(err, "failed to send a new mining session message: %#v", sess), + errors.Wrapf(r.db.InsertTyped(fmt.Sprintf("MINING_SESSIONS_DLQ_%v", r.workerIndex(ctx)), dlq, &[]*MiningSessionDLQ{}), + "failed to dlqMiningSessionMessage:%#v because sendMiningSessionMessage failed", sess), + ).ErrorOrNil() + } + + return nil +} + +func (r *repository) sendMiningSessionMessage(ctx context.Context, ms *MiningSession) error { + valueBytes, err := json.MarshalContext(ctx, ms) + if err != nil { + return errors.Wrapf(err, "failed to marshal %#v", ms) + } + msg := &messagebroker.Message{ + Timestamp: *ms.LastNaturalMiningStartedAt.Time, + Headers: map[string]string{"producer": "freezer"}, + Key: *ms.UserID, + Topic: r.cfg.MessageBroker.Topics[2].Name, + Value: valueBytes, + } + responder := make(chan error, 1) + defer close(responder) + r.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send `%v` message to broker", msg.Topic) +} + +func (s *miningSessionsTableSource) Process(ctx context.Context, msg *messagebroker.Message) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline while processing message") + } + if len(msg.Value) == 0 { + return nil + } + + return errors.Wrapf(s.trySwitchToNextAdoption(ctx), "failed to trySwitchToNextAdoption") +} diff --git a/tokenomics/mining_sessions_test.go b/tokenomics/mining_sessions_test.go new file mode 100644 index 0000000..7184b18 --- /dev/null +++ b/tokenomics/mining_sessions_test.go @@ -0,0 +1,480 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "testing" + stdlibtime "time" + + "github.com/stretchr/testify/assert" + + appCfg "github.com/ice-blockchain/wintr/config" + "github.com/ice-blockchain/wintr/time" +) + +func TestRepositoryNewMiningSummary_CloseToMin(t *testing.T) { //nolint:funlen // . + t.Parallel() + var cfg config + appCfg.MustLoadFromKey(applicationYamlKey, &cfg) + repo := &repository{cfg: &cfg} + actual := make([]*miningSummary, 0, 27) + startDates := make([]*time.Time, 0, 27) + old := new(miningSummary) + now := time.Now() + for ii := 0; ii < 27; ii++ { + newMS := repo.newMiningSummary(old, now) + startDates = append(startDates, now) + actual = append(actual, newMS) + old = newMS + delta := stdlibtime.Duration(0) + if ii == 24 { + delta = repo.cfg.MiningSessionDuration.Min + repo.cfg.MiningSessionDuration.Max + 1 + } + if ii == 25 { + delta = repo.cfg.MiningSessionDuration.Min + 1 + } + now = time.New(now.Add(repo.cfg.MiningSessionDuration.Min).Add(delta)) + } + assert.Len(t, actual, 27) + ix := 0 + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 0, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 0, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 1, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 1, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 2, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 2, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 3, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 3, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 4, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 4, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 5, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 5, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 6, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-1], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 6, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-2], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 7, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-3], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 7, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-4], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 8, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-5], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 8, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-6], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 9, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-7], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 9, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-8], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 10, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-9], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 10, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-10], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 11, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-11], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 11, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 12, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-1], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 14, + }, actual[ix]) + delta := 1 * stdlibtime.Millisecond + assert.EqualValues(t, uint64(16), repo.calculateMiningStreak(time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(delta)), startDates[0], time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(delta)))) //nolint:lll // . + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[ix], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 0, + }, actual[ix]) +} + +func TestRepositoryNewMiningSummary_CloseToMax(t *testing.T) { //nolint:funlen,maintidx // . + t.Parallel() + var cfg config + appCfg.MustLoadFromKey(applicationYamlKey, &cfg) + repo := &repository{cfg: &cfg} + actual := make([]*miningSummary, 0, 27) + startDates := make([]*time.Time, 0, 27) + old := new(miningSummary) + now := time.Now() + for ii := 0; ii < 27; ii++ { + newMS := repo.newMiningSummary(old, now) + startDates = append(startDates, now) + actual = append(actual, newMS) + old = newMS + delta := stdlibtime.Duration(0) + if ii == 24 { + delta = 3*repo.cfg.MiningSessionDuration.Max - 1 + } + if ii == 25 { + delta = 2*repo.cfg.MiningSessionDuration.Max - 1 + } + now = time.New(now.Add(repo.cfg.MiningSessionDuration.Max - 1).Add(delta)) + } + assert.Len(t, actual, 27) + ix := 0 + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 0, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 0, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 1, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 2, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 3, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 4, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 5, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 6, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-1], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 7, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-2], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 8, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-3], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 9, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-4], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 10, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-5], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 11, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-6], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 12, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 13, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-1], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 14, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-2], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 15, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-3], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 16, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-4], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 17, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-5], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 18, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-6], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 19, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 20, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-1], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 21, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-2], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 22, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix-3], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), //nolint:lll // . + MiningStreak: 23, + }, actual[ix]) + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[0], + LastFreeMiningSessionAwardedAt: startDates[ix], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 27, + }, actual[ix]) + delta := 1 * stdlibtime.Millisecond + assert.EqualValues(t, uint64(30), repo.calculateMiningStreak(time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(delta)), startDates[0], time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Max).Add(delta)))) //nolint:lll // . + ix++ + assert.EqualValues(t, &miningSummary{ + LastNaturalMiningStartedAt: startDates[ix], + LastMiningStartedAt: startDates[ix], + LastMiningEndedAt: time.New(startDates[ix].Add(repo.cfg.MiningSessionDuration.Max)), + MiningStreak: 0, + }, actual[ix]) +} diff --git a/tokenomics/mining_test.go b/tokenomics/mining_test.go new file mode 100644 index 0000000..1451bb6 --- /dev/null +++ b/tokenomics/mining_test.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "testing" + stdlibtime "time" + + "github.com/stretchr/testify/assert" + + appCfg "github.com/ice-blockchain/wintr/config" + "github.com/ice-blockchain/wintr/time" +) + +func TestRepositoryCalculateMiningSession(t *testing.T) { + t.Parallel() + var cfg config + appCfg.MustLoadFromKey(applicationYamlKey, &cfg) + repo := &repository{cfg: &cfg} + + now := time.Now() + start := time.New(now.Add(-1 * stdlibtime.Second)) + end := time.New(now.Add(repo.cfg.MiningSessionDuration.Max).Add(-1 * stdlibtime.Second)) + actual := repo.calculateMiningSession(now, start, end) + assert.EqualValues(t, start, actual.StartedAt) + assert.False(t, *actual.Free) + + start = time.New(now.Add(-1 - repo.cfg.MiningSessionDuration.Min)) + end = time.New(now.Add(repo.cfg.MiningSessionDuration.Max).Add(-1 - repo.cfg.MiningSessionDuration.Min)) + actual = repo.calculateMiningSession(now, start, end) + assert.EqualValues(t, start, actual.StartedAt) + assert.False(t, *actual.Free) + + start = time.New(now.Add(-1 - repo.cfg.MiningSessionDuration.Max)) + end = time.New(now.Add(repo.cfg.MiningSessionDuration.Max).Add(repo.cfg.MiningSessionDuration.Min).Add(-1 - repo.cfg.MiningSessionDuration.Max)) + actual = repo.calculateMiningSession(now, start, end) + assert.EqualValues(t, time.New(start.Add(repo.cfg.MiningSessionDuration.Max)), actual.StartedAt) + assert.True(t, *actual.Free) +} diff --git a/tokenomics/pre_staking.go b/tokenomics/pre_staking.go new file mode 100644 index 0000000..b6ba840 --- /dev/null +++ b/tokenomics/pre_staking.go @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + + "github.com/goccy/go-json" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + + "github.com/ice-blockchain/go-tarantool-client" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/time" +) + +func (r *repository) GetPreStakingSummary(ctx context.Context, userID string) (*PreStakingSummary, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(`SELECT st.created_at, + st.user_id, + MAX(st.years) AS years, + MAX(st.allocation) AS allocation, + st_b.bonus + FROM pre_stakings_%[1]v st + LEFT JOIN pre_staking_bonuses st_b + ON st.years = st_b.years + WHERE st.user_id = :user_id + GROUP BY st.user_id`, r.workerIndex(ctx)) + params := make(map[string]any, 1) + params["user_id"] = userID + resp := make([]*PreStakingSummary, 0, 1) + if err := r.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return nil, errors.Wrapf(err, "failed to select for pre-staking summary for userID:%v", userID) + } + if len(resp) == 0 { + return nil, storage.ErrNotFound + } + resp[0].UserID = "" + resp[0].CreatedAt = nil + + return resp[0], nil +} + +func (r *repository) getAllPreStakingSummaries(ctx context.Context, userID string) (resp []*PreStakingSummary, err error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(`SELECT st.*, + st_b.bonus + FROM pre_stakings_%[1]v st + JOIN pre_staking_bonuses st_b + ON st.years = st_b.years + WHERE st.user_id = :user_id + ORDER BY st.created_at`, r.workerIndex(ctx)) + params := make(map[string]any, 1) + params["user_id"] = userID + err = errors.Wrapf(r.db.PrepareExecuteTyped(sql, params, &resp), "failed to select all pre-staking summaries for userID:%v", userID) + + return +} + +func (r *repository) StartOrUpdatePreStaking(ctx context.Context, st *PreStakingSummary) error { //nolint:funlen,gocognit // Can't properly split it further. + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + existing, err := r.GetPreStakingSummary(ctx, st.UserID) + if err != nil && !errors.Is(err, storage.ErrNotFound) { + return errors.Wrapf(err, "failed to GetPreStakingSummary for userID:%v", st.UserID) + } + if existing != nil { + if (existing.Allocation == percentage100 || existing.Allocation == st.Allocation) && + (existing.Years == MaxPreStakingYears || existing.Years == st.Years) { + *st = *existing + + return nil + } + if existing.Allocation > st.Allocation || existing.Years > st.Years { + return ErrDecreasingPreStakingAllocationOrYearsNotAllowed + } + } + st.CreatedAt = time.Now() + sql := fmt.Sprintf(`INSERT INTO pre_stakings_%[1]v (created_at,user_id,years,allocation) + VALUES (:created_at,:user_id,:years,:allocation)`, r.workerIndex(ctx)) + params := make(map[string]any, 1+1+1+1) + params["created_at"] = st.CreatedAt + params["user_id"] = st.UserID + params["years"] = st.Years + params["allocation"] = st.Allocation + if err = storage.CheckSQLDMLErr(r.db.PrepareExecute(sql, params)); err != nil { + return errors.Wrapf(err, "failed to insertNewPreStaking:%#v", st) + } + var res struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:tagliatelle,revive,nosnakecase // To insert we need asArray + Years, Bonus uint64 + } + if err = r.db.GetTyped("PRE_STAKING_BONUSES", "pk_unnamed_PRE_STAKING_BONUSES_1", tarantool.UintKey{I: uint(st.Years)}, &res); err != nil { + return errors.Wrapf(err, "failed to get pre-staking bonus for years:%v", st.Years) + } + st.Bonus = res.Bonus + ss := &PreStakingSnapshot{PreStakingSummary: st, Before: existing} + if err = r.sendPreStakingSnapshotMessage(ctx, ss); err != nil { + pkIndex := fmt.Sprintf("pk_unnamed_PRE_STAKINGS_%v_1", r.workerIndex(ctx)) + key := []any{st.UserID, st.Years, st.Allocation} + + return multierror.Append( //nolint:wrapcheck // Not needed. + errors.Wrapf(err, "failed to send pre-staking snapshot message:%#v", ss), + errors.Wrapf(r.db.DeleteTyped(fmt.Sprintf("PRE_STAKINGS_%v", r.workerIndex(ctx)), pkIndex, key, &[]*PreStaking{}), + "failed to revertInsertNewPreStaking: %#v", st), + ).ErrorOrNil() + } + st.UserID = "" + st.CreatedAt = nil + + return nil +} + +func (r *repository) sendPreStakingSnapshotMessage(ctx context.Context, st *PreStakingSnapshot) error { + valueBytes, err := json.MarshalContext(ctx, st) + if err != nil { + return errors.Wrapf(err, "failed to marshal %#v", st) + } + msg := &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: st.UserID, + Topic: r.cfg.MessageBroker.Topics[6].Name, + Value: valueBytes, + } + responder := make(chan error, 1) + defer close(responder) + r.mb.SendMessage(ctx, msg, responder) + + return errors.Wrapf(<-responder, "failed to send `%v` message to broker", msg.Topic) +} diff --git a/tokenomics/seeding/seeding.go b/tokenomics/seeding/seeding.go new file mode 100644 index 0000000..2e5df78 --- /dev/null +++ b/tokenomics/seeding/seeding.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: ice License 1.0 + +//go:build !test + +package seeding + +import ( + "fmt" + "os" + "strings" + stdlibtime "time" + + "github.com/ice-blockchain/go-tarantool-client" + "github.com/ice-blockchain/wintr/log" +) + +func StartSeeding() { + before := stdlibtime.Now() + db := dbConnector() + defer func() { + log.Panic(db.Close()) //nolint:revive // It doesnt really matter. + log.Info(fmt.Sprintf("seeding finalized in %v", stdlibtime.Since(before).String())) + }() + log.Info("TODO: implement seeding") +} + +func cleanUpWorkerSpaces(db tarantool.Connector) { //nolint:deadcode,unused // . + tables := []string{ + "balance_recalculation_worker_", + "extra_bonus_processing_worker_", + "blockchain_balance_synchronization_worker_", + "mining_rates_recalculation_worker_", + "balance_recalculation_worker_", + "pre_stakings_", + "mining_sessions_dlq_", + } + for _, table := range tables { + for i := 0; i < 1000; i++ { + sql := fmt.Sprintf(`delete from %[1]v%[2]v where 1=1`, table, i) + _, err := db.PrepareExecute(sql, map[string]any{}) + log.Panic(err) + } + } +} + +func dbConnector() tarantool.Connector { + parts := strings.Split(os.Getenv("MASTER_DB_INSTANCE_ADDRESS"), "@") + userAndPass := strings.Split(parts[0], ":") + opts := tarantool.Opts{ + Timeout: 20 * stdlibtime.Second, //nolint:gomnd // It doesnt matter here. + Reconnect: stdlibtime.Millisecond, + MaxReconnects: 10, //nolint:gomnd // It doesnt matter here. + User: userAndPass[0], + Pass: userAndPass[1], + } + db, err := tarantool.Connect(parts[1], opts) + log.Panic(err) + + return db +} diff --git a/tokenomics/tokenomics.go b/tokenomics/tokenomics.go new file mode 100644 index 0000000..ed157ee --- /dev/null +++ b/tokenomics/tokenomics.go @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + "strings" + "sync" + stdlibtime "time" + + "github.com/cenkalti/backoff/v4" + "github.com/goccy/go-json" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/go-tarantool-client" + appCfg "github.com/ice-blockchain/wintr/config" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" + "github.com/ice-blockchain/wintr/log" + "github.com/ice-blockchain/wintr/multimedia/picture" + "github.com/ice-blockchain/wintr/time" +) + +func New(ctx context.Context, cancel context.CancelFunc) Repository { + var cfg config + appCfg.MustLoadFromKey(applicationYamlKey, &cfg) + + db := storage.MustConnect(ctx, cancel, getDDL(&cfg), applicationYamlKey) + + return &repository{ + cfg: &cfg, + shutdown: db.Close, + db: db, + pictureClient: picture.New(applicationYamlKey), + } +} + +func StartProcessor(ctx context.Context, cancel context.CancelFunc) Processor { //nolint:funlen // A lot of startup & shutdown ceremony. + var cfg config + appCfg.MustLoadFromKey(applicationYamlKey, &cfg) + + var mbConsumer messagebroker.Client + prc := &processor{repository: &repository{ + cfg: &cfg, + db: storage.MustConnect(context.Background(), func() { //nolint:contextcheck // It's intended. Cuz we want to close everything gracefully. + if mbConsumer != nil { + log.Error(errors.Wrap(mbConsumer.Close(), "failed to close mbConsumer due to db premature cancellation")) + } + cancel() + }, getDDL(&cfg), applicationYamlKey), + mb: messagebroker.MustConnect(ctx, applicationYamlKey), + pictureClient: picture.New(applicationYamlKey), + }} + //nolint:contextcheck // It's intended. Cuz we want to close everything gracefully. + mbConsumer = messagebroker.MustConnectAndStartConsuming(context.Background(), cancel, applicationYamlKey, + &usersTableSource{processor: prc}, + &globalTableSource{processor: prc}, + &miningSessionsTableSource{processor: prc}, + &addBalanceCommandsSource{processor: prc}, + &viewedNewsSource{processor: prc}, + &deviceMetadataTableSource{processor: prc}, + &balanceRecalculationTriggerStreamSource{processor: prc}, + &miningRatesRecalculationTriggerStreamSource{processor: prc}, + &blockchainBalanceSynchronizationTriggerStreamSource{processor: prc}, + &extraBonusProcessingTriggerStreamSource{processor: prc}, + ) + prc.shutdown = closeAll(mbConsumer, prc.mb, prc.db) + + prc.initializeExtraBonusWorkers() + prc.mustNotifyCurrentAdoption(ctx) + go prc.startBalanceRecalculationTriggerSeedingStream(ctx) + go prc.startMiningRatesRecalculationTriggerSeedingStream(ctx) + go prc.startBlockchainBalanceSynchronizationTriggerSeedingStream(ctx) + go prc.startExtraBonusProcessingTriggerSeedingStream(ctx) + + return prc +} + +func getDDL(cfg *config) string { + extraBonusesValues := make([]string, 0, len(cfg.ExtraBonuses.FlatValues)) + for ix, value := range cfg.ExtraBonuses.FlatValues { + extraBonusesValues = append(extraBonusesValues, fmt.Sprintf("(%v,%v)", ix, value)) + } + now := time.Now() + adoptionStart := now.Add(-24 * stdlibtime.Hour).UnixNano() + dailyBonusStart := now.Add(-1 * users.NanosSinceMidnight(now)).UnixNano() + args := make([]any, 0, len(cfg.AdoptionMilestoneSwitch.ActiveUserMilestones)+1+1+1+1) + args = append(args, adoptionStart, cfg.WorkerCount-1, strings.Join(extraBonusesValues, ","), dailyBonusStart) + for ix := range cfg.AdoptionMilestoneSwitch.ActiveUserMilestones { + args = append(args, cfg.AdoptionMilestoneSwitch.ActiveUserMilestones[ix]) + } + + return fmt.Sprintf(ddl, args...) +} + +func (r *repository) Close() error { + return errors.Wrap(r.shutdown(), "closing repository failed") +} + +func closeAll(mbConsumer, mbProducer messagebroker.Client, db tarantool.Connector, otherClosers ...func() error) func() error { + return func() error { + err1 := errors.Wrap(mbConsumer.Close(), "closing message broker consumer connection failed") + err2 := errors.Wrap(db.Close(), "closing db connection failed") + err3 := errors.Wrap(mbProducer.Close(), "closing message broker producer connection failed") + errs := make([]error, 0, 1+1+1+len(otherClosers)) + errs = append(errs, err1, err2, err3) + for _, closeOther := range otherClosers { + if err := closeOther(); err != nil { + errs = append(errs, err) + } + } + + return errors.Wrap(multierror.Append(nil, errs...).ErrorOrNil(), "failed to close resources") + } +} + +func (p *processor) CheckHealth(ctx context.Context) error { + if _, err := p.db.Ping(); err != nil { + return errors.Wrap(err, "[health-check] failed to ping DB") + } + type ts struct { + TS *time.Time `json:"ts"` + } + now := ts{TS: time.Now()} + bytes, err := json.MarshalContext(ctx, now) + if err != nil { + return errors.Wrapf(err, "[health-check] failed to marshal %#v", now) + } + responder := make(chan error, 1) + p.mb.SendMessage(ctx, &messagebroker.Message{ + Headers: map[string]string{"producer": "freezer"}, + Key: p.cfg.MessageBroker.Topics[0].Name, + Topic: p.cfg.MessageBroker.Topics[0].Name, + Value: bytes, + }, responder) + + return errors.Wrapf(<-responder, "[health-check] failed to send health check message to broker") +} + +func retry(ctx context.Context, op func() error) error { + //nolint:wrapcheck // No need, its just a proxy. + return backoff.RetryNotify( + op, + //nolint:gomnd // Because those are static configs. + backoff.WithContext(&backoff.ExponentialBackOff{ + InitialInterval: 100 * stdlibtime.Millisecond, + RandomizationFactor: 0.5, + Multiplier: 2.5, + MaxInterval: stdlibtime.Second, + MaxElapsedTime: 25 * stdlibtime.Second, + Stop: backoff.Stop, + Clock: backoff.SystemClock, + }, ctx), + func(e error, next stdlibtime.Duration) { + log.Error(errors.Wrapf(e, "call failed. retrying in %v... ", next)) + }) +} + +func ContextWithHashCode(ctx context.Context, hashCode uint64) context.Context { + if hashCode == 0 { + return ctx + } + + return context.WithValue(ctx, userHashCodeCtxValueKey, hashCode) //nolint:revive,staticcheck // Not an issue. +} + +func requestingUserID(ctx context.Context) (requestingUserID string) { + requestingUserID, _ = ctx.Value(requestingUserIDCtxValueKey).(string) //nolint:errcheck // Not needed. + + return +} + +func (r *repository) workerIndex(ctx context.Context) (workerIndex uint64) { + userHashCode, _ := ctx.Value(userHashCodeCtxValueKey).(uint64) //nolint:errcheck // Not needed. + + return userHashCode % r.cfg.WorkerCount +} + +func sendMessagesConcurrently[M any](ctx context.Context, sendMessage func(context.Context, M) error, messages []M) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + if len(messages) == 0 { + return nil + } + wg := new(sync.WaitGroup) + wg.Add(len(messages)) + errChan := make(chan error, len(messages)) + for i := range messages { + go func(ix int) { + defer wg.Done() + errChan <- errors.Wrapf(sendMessage(ctx, messages[ix]), "failed to sendMessage:%#v", messages[ix]) + }(i) + } + wg.Wait() + close(errChan) + errs := make([]error, 0, len(messages)) + for err := range errChan { + errs = append(errs, err) + } + + return errors.Wrap(multierror.Append(nil, errs...).ErrorOrNil(), "at least one message sends failed") +} + +func (c *config) globalAggregationIntervalChildDateFormat() string { + const hoursInADay = 24 + switch c.GlobalAggregationInterval.Child { //nolint:exhaustive // We don't care about the others. + case stdlibtime.Minute: + return minuteFormat + case stdlibtime.Hour: + return hourFormat + case hoursInADay * stdlibtime.Hour: + return dayFormat + default: + log.Panic(fmt.Sprintf("invalid interval: %v", c.GlobalAggregationInterval.Child)) + + return "" + } +} + +func (c *config) globalAggregationIntervalParentDateFormat() string { + const hoursInADay = 24 + switch c.GlobalAggregationInterval.Parent { //nolint:exhaustive // We don't care about the others. + case stdlibtime.Minute: + return minuteFormat + case stdlibtime.Hour: + return hourFormat + case hoursInADay * stdlibtime.Hour: + return dayFormat + default: + log.Panic(fmt.Sprintf("invalid interval: %v", c.GlobalAggregationInterval.Parent)) + + return "" + } +} + +func (c *config) lastXMiningSessionsCollectingIntervalDateFormat() string { + const hoursInADay = 24 + switch c.RollbackNegativeMining.LastXMiningSessionsCollectingInterval { //nolint:exhaustive // We don't care about the others. + case stdlibtime.Minute: + return minuteFormat + case stdlibtime.Hour: + return hourFormat + case hoursInADay * stdlibtime.Hour: + return dayFormat + default: + log.Panic(fmt.Sprintf("invalid interval: %v", c.RollbackNegativeMining.LastXMiningSessionsCollectingInterval)) + + return "" + } +} + +func (r *repository) lastXMiningSessionsCollectingIntervalDateFormat(now *time.Time) string { + return now.Format(r.cfg.lastXMiningSessionsCollectingIntervalDateFormat()) +} diff --git a/tokenomics/users.go b/tokenomics/users.go new file mode 100644 index 0000000..ace506e --- /dev/null +++ b/tokenomics/users.go @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + + "github.com/goccy/go-json" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/go-tarantool-client" + "github.com/ice-blockchain/wintr/coin" + messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" + "github.com/ice-blockchain/wintr/connectors/storage" +) + +func (s *usersTableSource) Process(ctx context.Context, msg *messagebroker.Message) error { //nolint:gocognit // . + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline while processing message") + } + if len(msg.Value) == 0 { + return nil + } + var usr users.UserSnapshot + if err := json.UnmarshalContext(ctx, msg.Value, &usr); err != nil { + return errors.Wrapf(err, "process: cannot unmarshall %v into %#v", string(msg.Value), &usr) + } + if (usr.User == nil || usr.User.ID == "") && (usr.Before == nil || usr.Before.ID == "") { + return nil + } + + if usr.User == nil || usr.User.ID == "" { + return errors.Wrapf(s.deleteUser(ctx, usr.Before), "failed to delete user:%#v", usr.Before) + } + + if err := s.replaceUser(ctx, usr.User); err != nil { + return errors.Wrapf(err, "failed to replace user:%#v", usr.User) + } + + return nil +} + +func (s *usersTableSource) deleteUser(ctx context.Context, usr *users.User) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + if err := s.removeBalanceFromT0AndTMinus1(ctx, usr); err != nil { + return errors.Wrapf(err, "failed to removeBalanceFromT0AndTMinus1 for user:%#v", usr) + } + sql := `DELETE FROM users + WHERE user_id = :user_id` + params := make(map[string]any, 1) + params["user_id"] = usr.ID + if _, err := storage.CheckSQLDMLResponse(s.db.PrepareExecute(sql, params)); err != nil { + return errors.Wrapf(err, "failed to delete userID:%v", usr.ID) + } + + return nil +} + +func (s *usersTableSource) removeBalanceFromT0AndTMinus1(ctx context.Context, usr *users.User) error { //nolint:funlen // . + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(`SELECT reverse_t0_balance.amount, + reverse_tminus1_balance.amount, + negative_t0_balance.amount, + negative_tminus1_balance.amount, + t0.user_id, + tminus1.user_id + FROM users u + JOIN users t0 + ON t0.user_id = u.referred_by + AND t0.user_id != u.user_id + JOIN users tminus1 + ON tminus1.user_id = t0.referred_by + AND tminus1.user_id != t0.user_id + LEFT JOIN balances_%[1]v reverse_t0_balance + ON reverse_t0_balance.user_id = u.user_id + AND reverse_t0_balance.negative = FALSE + AND reverse_t0_balance.type = %[2]v + AND reverse_t0_balance.type_detail = '%[3]v_' || t0.user_id + LEFT JOIN balances_%[1]v reverse_tminus1_balance + ON reverse_tminus1_balance.user_id = u.user_id + AND reverse_tminus1_balance.negative = FALSE + AND reverse_tminus1_balance.type = %[2]v + AND reverse_tminus1_balance.type_detail = '%[4]v_' || tminus1.user_id + LEFT JOIN balances_%[1]v negative_t0_balance + ON negative_t0_balance.user_id = u.user_id + AND negative_t0_balance.negative = TRUE + AND negative_t0_balance.type = %[2]v + AND negative_t0_balance.type_detail = '%[3]v_' || t0.user_id + LEFT JOIN balances_%[1]v negative_tminus1_balance + ON negative_tminus1_balance.user_id = u.user_id + AND negative_tminus1_balance.negative = TRUE + AND negative_tminus1_balance.type = %[2]v + AND negative_tminus1_balance.type_detail = '%[4]v_' || tminus1.user_id + WHERE u.user_id = :user_id`, + usr.HashCode%s.cfg.WorkerCount, + totalNoPreStakingBonusBalanceType, + reverseT0BalanceTypeDetail, + reverseTMinus1BalanceTypeDetail) + params := make(map[string]any, 1) + params["user_id"] = usr.ID + type resp struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:unused,tagliatelle,revive,nosnakecase // . + TotalReverseT0Amount, TotalReverseTMinus1Amount, + NegativeReverseT0Amount, NegativeReverseTMinus1Amount *coin.ICEFlake + T0UserID, TMinus1UserID string + } + res := make([]*resp, 0, 1) + if err := s.db.PrepareExecuteTyped(sql, params, &res); err != nil { + return errors.Wrapf(err, "failed to get reverse t0 and t-1 balance information for userID:%v", usr.ID) + } + if len(res) == 0 { + return nil + } + cmds := make([]*AddBalanceCommand, 0, 1+1+1+1) + if !res[0].TotalReverseT0Amount.IsZero() { + cmds = append(cmds, &AddBalanceCommand{ + Balances: &Balances[coin.ICEFlake]{ + T1: res[0].TotalReverseT0Amount, + UserID: res[0].T0UserID, + }, + EventID: fmt.Sprintf("t1_referral_account_deletion_positive_balance_%v", usr.ID), + }) + } + if !res[0].NegativeReverseT0Amount.IsZero() { + negative := true + cmds = append(cmds, &AddBalanceCommand{ + Balances: &Balances[coin.ICEFlake]{ + T1: res[0].NegativeReverseT0Amount, + UserID: res[0].T0UserID, + }, + EventID: fmt.Sprintf("t1_referral_account_deletion_negative_balance_%v", usr.ID), + Negative: &negative, + }) + } + if !res[0].TotalReverseTMinus1Amount.IsZero() { + cmds = append(cmds, &AddBalanceCommand{ + Balances: &Balances[coin.ICEFlake]{ + T2: res[0].TotalReverseTMinus1Amount, + UserID: res[0].TMinus1UserID, + }, + EventID: fmt.Sprintf("t2_referral_account_deletion_positive_balance_%v", usr.ID), + }) + } + if !res[0].NegativeReverseTMinus1Amount.IsZero() { + negative := true + cmds = append(cmds, &AddBalanceCommand{ + Balances: &Balances[coin.ICEFlake]{ + T2: res[0].NegativeReverseTMinus1Amount, + UserID: res[0].TMinus1UserID, + }, + EventID: fmt.Sprintf("t2_referral_account_deletion_negative_balance_%v", usr.ID), + Negative: &negative, + }) + } + + return errors.Wrapf(sendMessagesConcurrently(ctx, s.sendAddBalanceCommandMessage, cmds), "failed to sendAddBalanceCommandMessages for %#v", cmds) +} + +func (s *usersTableSource) replaceUser(ctx context.Context, usr *users.User) (err error) { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + if err = s.updateUser(ctx, usr); err != nil { + if errors.Is(err, storage.ErrNotFound) { + err = errors.Wrapf(s.insertUser(ctx, usr), "failed to insert user:%#v", usr) + } + } + + return errors.Wrapf(err, "failed to update user:%#v", usr) +} + +func (s *usersTableSource) updateUser(ctx context.Context, usr *users.User) (err error) { //nolint:funlen // . + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + const fieldCount = 10 + verified := false + if usr.Verified != nil && *usr.Verified { + verified = true + } + ops := make([]tarantool.Op, 0, fieldCount) + //nolint:gomnd // Not magic numbers, those are field indices. + ops = append(ops, + tarantool.Op{Op: "=", Field: 1, Arg: usr.UpdatedAt}, + tarantool.Op{Op: "=", Field: 10, Arg: usr.ReferredBy}, + tarantool.Op{Op: "=", Field: 11, Arg: usr.Username}, + tarantool.Op{Op: "=", Field: 12, Arg: usr.FirstName}, + tarantool.Op{Op: "=", Field: 13, Arg: usr.LastName}, + tarantool.Op{Op: "=", Field: 14, Arg: s.pictureClient.StripDownloadURL(usr.ProfilePictureURL)}, + tarantool.Op{Op: "=", Field: 15, Arg: usr.MiningBlockchainAccountAddress}, + tarantool.Op{Op: "=", Field: 16, Arg: usr.BlockchainAccountAddress}, + tarantool.Op{Op: "=", Field: 18, Arg: s.hideRanking(usr)}, + tarantool.Op{Op: "=", Field: 19, Arg: verified}) + res := make([]*user, 0, 1) + key := tarantool.StringKey{S: usr.ID} + if err = storage.CheckNoSQLDMLErr(s.db.UpdateTyped("USERS", "pk_unnamed_USERS_1", key, ops, &res)); err == nil && (len(res) == 0 || res[0].UserID == "") { //nolint:lll,revive // Wrong. + err = storage.ErrNotFound + } + if err == nil { + if err = s.updateBlockchainBalanceSynchronizationWorkerBlockchainAccountAddress(ctx, usr); err != nil { + err = errors.Wrapf(err, "failed to updateBlockchainBalanceSynchronizationWorkerBlockchainAccountAddress for usr:%#v", usr) + } + } + + return err +} + +func (s *usersTableSource) insertUser(ctx context.Context, usr *users.User) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + if err := storage.CheckNoSQLDMLErr(s.db.InsertTyped("USERS", s.user(usr), &[]*user{})); err != nil { + if errors.Is(err, storage.ErrDuplicate) { + return s.updateUser(ctx, usr) + } + + return errors.Wrapf(err, "failed to insert user %#v", usr) + } + if err := s.doAfterCreate(ctx, usr); err != nil { + revertCtx, cancel := context.WithTimeout(context.Background(), requestDeadline) + defer cancel() + revertErr := errors.Wrapf(s.deleteUser(revertCtx, usr), //nolint:contextcheck // It might be cancelled. + "failed to delete userID:%v as a rollback for failed doAfterCreate", usr.ID) + if revertErr != nil && errors.Is(revertErr, storage.ErrNotFound) { + revertErr = nil + } + + return multierror.Append( //nolint:wrapcheck // Not needed. + errors.Wrapf(err, "failed to run doAfterCreate for:%#v", usr), + revertErr, + ).ErrorOrNil() + } + + return nil +} + +func (s *usersTableSource) user(usr *users.User) *user { + verified := false + if usr.Verified != nil && *usr.Verified { + verified = true + } + + return &user{ + CreatedAt: usr.CreatedAt, + UpdatedAt: usr.UpdatedAt, + UserID: usr.ID, + ReferredBy: usr.ReferredBy, + Username: usr.Username, + FirstName: usr.FirstName, + LastName: usr.LastName, + ProfilePictureURL: s.pictureClient.StripDownloadURL(usr.ProfilePictureURL), + MiningBlockchainAccountAddress: usr.MiningBlockchainAccountAddress, + BlockchainAccountAddress: usr.BlockchainAccountAddress, + HashCode: usr.HashCode, + HideRanking: s.hideRanking(usr), + Verified: verified, + } +} + +func (*usersTableSource) hideRanking(usr *users.User) (hideRanking bool) { + if usr.HiddenProfileElements != nil { + for _, element := range *usr.HiddenProfileElements { + if users.GlobalRankHiddenProfileElement == element { + hideRanking = true + + break + } + } + } + + return hideRanking +} + +func (s *usersTableSource) doAfterCreate(ctx context.Context, usr *users.User) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + if err := s.initializeBalanceRecalculationWorker(ctx, usr); err != nil { + return errors.Wrapf(err, "failed to initializeBalanceRecalculationWorker for %#v", usr) + } + + if err := s.initializeMiningRatesRecalculationWorker(ctx, usr); err != nil { + return errors.Wrapf(err, "failed to initializeMiningRatesRecalculationWorker for %#v", usr) + } + + if err := s.initializeBlockchainBalanceSynchronizationWorker(ctx, usr); err != nil { + return errors.Wrapf(err, "failed to initializeBlockchainBalanceSynchronizationWorker for %#v", usr) + } + + if err := s.initializeExtraBonusProcessingWorker(ctx, usr); err != nil { + return errors.Wrapf(err, "failed to initializeExtraBonusProcessingWorker for %#v", usr) + } + + return errors.Wrapf(s.awardRegistrationICECoinsBonus(ctx, usr), "failed to awardRegistrationBonus for %#v", usr) +} + +func (s *usersTableSource) awardRegistrationICECoinsBonus(ctx context.Context, usr *users.User) error { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + cmd := &AddBalanceCommand{ + Balances: &Balances[coin.ICEFlake]{ + Total: coin.NewAmountUint64(registrationICEFlakeBonusAmount), + UserID: usr.ID, + }, + EventID: "registration_ice_bonus", + } + + return errors.Wrapf(s.sendAddBalanceCommandMessage(ctx, cmd), "failed to sendAddBalanceCommandMessage for %#v", cmd) +} diff --git a/tokenomics/worker.go b/tokenomics/worker.go new file mode 100644 index 0000000..3442368 --- /dev/null +++ b/tokenomics/worker.go @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: ice License 1.0 + +package tokenomics + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/wintr/connectors/storage" +) + +func (r *repository) initializeWorker(ctx context.Context, table, userID string, workerIndex uint64) (err error) { + if ctx.Err() != nil { + return errors.Wrap(ctx.Err(), "unexpected deadline") + } + sql := fmt.Sprintf(`INSERT INTO %v%v(user_id) VALUES (:user_id)`, table, workerIndex) + params := make(map[string]any, 1) + params["user_id"] = userID + if err = storage.CheckSQLDMLErr(r.db.PrepareExecute(sql, params)); err != nil && errors.Is(err, storage.ErrDuplicate) { + return nil + } + + return errors.Wrapf(err, "failed to %v, for userID:%v", sql, userID) +} + +func (r *repository) updateWorkerFields( + ctx context.Context, workerIndex uint64, table string, updateKV map[string]any, userIDs ...string, +) (err error) { + if ctx.Err() != nil || len(userIDs) == 0 { + return errors.Wrap(ctx.Err(), "context failed") + } + values := make([]string, 0, len(userIDs)) + fields := make([]string, 0, len(updateKV)) + params := make(map[string]any, len(userIDs)+len(updateKV)) + for key, value := range updateKV { + if value == nil { + fields = append(fields, fmt.Sprintf("%[1]v = null", key)) + } else { + params[key] = value + fields = append(fields, fmt.Sprintf("%[1]v = :%[1]v", key)) + } + } + for i := range userIDs { + params[fmt.Sprintf("user_id%v", i)] = userIDs[i] + values = append(values, fmt.Sprintf(":user_id%v", i)) + } + sql := fmt.Sprintf(`UPDATE %[1]v%[2]v + SET %[3]v + WHERE user_id in (%[4]v)`, table, workerIndex, strings.Join(fields, ","), strings.Join(values, ",")) + if _, uErr := storage.CheckSQLDMLResponse(r.db.PrepareExecute(sql, params)); uErr != nil { + return errors.Wrapf(uErr, "failed to UPDATE %v%v params :%#v, for userIDs:%#v", table, workerIndex, params, userIDs) + } + + return nil +} + +func (r *repository) getWorkerIndex(ctx context.Context, userID string) (uint64, error) { + if ctx.Err() != nil { + return 0, errors.Wrap(ctx.Err(), "context failed") + } + sql := `SELECT hash_code % :workers FROM users where user_id = :user_id` + params := make(map[string]any, 1+1) + params["workers"] = r.cfg.WorkerCount + params["user_id"] = userID + resp := make([]*struct { + _msgpack struct{} `msgpack:",asArray"` //nolint:tagliatelle,revive,nosnakecase // To insert we need asArray + WorkerIndex uint64 + }, 0, 1) + if err := r.db.PrepareExecuteTyped(sql, params, &resp); err != nil { + return 0, errors.Wrapf(err, "failed to get worker index for userID:%v", userID) + } + if len(resp) == 0 { + return 0, ErrNotFound + } + + return resp[0].WorkerIndex, nil +}