From e77d1ab79377b706025d22f3a49717baccb3951a Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 10 Mar 2026 12:31:09 +0100 Subject: [PATCH] linting --- .gitignore | 2 + Makefile | 20 +- api/billing/documents/.golangci.yml | 187 ++---------------- .../documents/internal/docstore/local.go | 44 ++++- api/billing/documents/internal/docstore/s3.go | 6 +- .../internal/service/documents/service.go | 6 - .../service/documents/service_test.go | 28 --- .../internal/service/documents/template.go | 1 + .../documents/renderer/renderer_test.go | 2 +- api/billing/fees/.golangci.yml | 187 ++---------------- api/discovery/.golangci.yml | 187 ++---------------- api/edge/bff/.golangci.yml | 47 +++++ .../accountservice/internal/service.go | 6 +- .../interface/api/srequest/endpoint_union.go | 2 + .../bff/interface/api/sresponse/payment.go | 6 +- api/edge/bff/interface/model/token.go | 2 +- .../bff/internal/api/discovery_resolver.go | 16 +- api/edge/bff/internal/api/ws/dispimp.go | 1 + api/edge/bff/internal/mutil/param/getter.go | 1 + .../bff/internal/mutil/param/getter_test.go | 10 +- .../internal/server/accountapiimp/delete.go | 2 +- .../internal/server/accountapiimp/password.go | 2 +- .../server/accountapiimp/password_test.go | 7 +- .../internal/server/accountapiimp/signup.go | 2 +- .../accountapiimp/signup_integration_test.go | 4 +- .../internal/server/callbacksimp/create.go | 2 +- .../internal/server/callbacksimp/update.go | 2 +- .../server/fileserviceimp/storage/localfs.go | 41 +++- .../fileserviceimp/storage/localfs_test.go | 35 ++-- .../internal/server/ledgerapiimp/create.go | 4 +- .../internal/server/papitemplate/archive.go | 2 +- .../server/paymentapiimp/documents.go | 29 ++- .../bff/internal/server/paymentapiimp/list.go | 2 + .../bff/internal/server/paymentapiimp/pay.go | 4 +- .../internal/server/paymentapiimp/pay_test.go | 2 +- .../internal/server/paymentapiimp/paybatch.go | 4 +- .../server/paymentapiimp/paybatch_test.go | 6 +- .../internal/server/paymentapiimp/quote.go | 8 +- .../internal/server/paymentapiimp/service.go | 3 +- .../server/permissionsimp/changepolicies.go | 2 +- .../internal/server/walletapiimp/balance.go | 7 +- .../internal/server/walletapiimp/create.go | 6 +- .../bff/internal/server/walletapiimp/list.go | 6 +- .../internal/server/walletapiimp/routing.go | 3 + .../internal/server/walletapiimp/service.go | 2 +- api/edge/bff/main.go | 4 +- api/edge/callbacks/.golangci.yml | 47 +++++ api/edge/callbacks/internal/config/service.go | 1 + .../callbacks/internal/delivery/service.go | 6 +- api/edge/callbacks/internal/ingest/service.go | 2 +- api/edge/callbacks/internal/retry/service.go | 1 + .../internal/server/internal/serverimp.go | 1 + .../callbacks/internal/storage/service.go | 4 +- api/fx/ingestor/.golangci.yml | 187 ++---------------- api/fx/ingestor/internal/config/config.go | 15 +- .../internal/ingestor/service_test.go | 14 +- .../internal/market/binance/connector.go | 6 +- .../ingestor/internal/market/cbr/connector.go | 26 ++- .../internal/market/cbr/http_client.go | 78 +++++++- .../internal/market/coingecko/connector.go | 6 +- .../internal/market/common/settings.go | 3 +- api/fx/ingestor/main.go | 4 +- api/fx/oracle/.golangci.yml | 47 +++++ api/fx/oracle/client/client.go | 26 ++- .../oracle/internal/service/oracle/cross.go | 20 +- .../oracle/internal/service/oracle/service.go | 2 +- .../internal/service/oracle/service_test.go | 2 +- api/fx/storage/.golangci.yml | 47 +++++ api/fx/storage/model/quote.go | 4 +- api/fx/storage/mongo/store/currency_test.go | 4 +- api/fx/storage/mongo/store/pair_test.go | 4 +- api/fx/storage/mongo/store/rates_test.go | 4 +- .../mongo/store/testing_helpers_test.go | 8 +- api/gateway/aurora/.golangci.yml | 47 +++++ api/gateway/aurora/client/client.go | 2 +- .../internal/server/internal/serverimp.go | 11 +- .../service/gateway/card_payout_store_test.go | 6 +- .../service/gateway/card_processor.go | 23 +-- .../service/gateway/card_processor_test.go | 23 ++- .../gateway/card_tokenize_validation_test.go | 4 +- .../internal/service/gateway/connector.go | 4 +- .../service/gateway/payout_execution_mode.go | 7 +- .../gateway/payout_failure_policy_test.go | 1 - .../service/gateway/scenario_simulator.go | 4 +- .../internal/service/gateway/service.go | 2 +- .../service/gateway/testhelpers_test.go | 6 +- .../service/gateway/transfer_notifications.go | 2 +- api/gateway/chain/.golangci.yml | 47 +++++ api/gateway/chain/client/client.go | 2 + .../internal/keymanager/vault/manager.go | 2 +- .../internal/server/internal/serverimp.go | 1 + .../gateway/commands/transfer/gas_topup.go | 1 + .../service/gateway/commands/wallet/create.go | 2 +- .../internal/service/gateway/connector.go | 51 +++-- .../service/gateway/driver/evm/evm.go | 26 +-- .../service/gateway/outbox_reliable.go | 6 +- .../service/gateway/rpcclient/clients.go | 4 +- .../chain/internal/service/gateway/service.go | 4 +- .../internal/service/gateway/service_test.go | 7 +- .../internal/service/gateway/shared/hex.go | 7 +- .../service/gateway/transfer_execution.go | 16 +- .../service/gateway/transfer_notifications.go | 14 +- api/gateway/chain/storage/model/wallet.go | 3 +- .../chain/storage/mongo/store/wallets.go | 1 - api/gateway/chsettle/.golangci.yml | 47 +++++ .../service/gateway/confirmation_flow.go | 8 +- .../service/gateway/scenario_simulator.go | 6 +- .../internal/service/gateway/service.go | 21 +- .../internal/service/gateway/service_test.go | 14 +- .../service/gateway/transfer_notifications.go | 2 +- .../internal/service/treasury/bot/router.go | 3 +- .../service/treasury/bot/router_test.go | 10 +- .../service/treasury/ledger/client.go | 2 +- .../treasury/ledger/discovery_client.go | 6 +- .../internal/service/treasury/module.go | 4 +- .../internal/service/treasury/service.go | 10 +- .../chsettle/storage/mongo/store/payments.go | 4 +- .../mongo/store/pending_confirmations.go | 4 +- .../storage/mongo/store/treasury_requests.go | 4 +- .../mongo/store/treasury_telegram_users.go | 4 +- api/gateway/common/.golangci.yml | 47 +++++ api/gateway/mntx/.golangci.yml | 47 +++++ api/gateway/mntx/client/client.go | 2 +- .../internal/server/internal/serverimp.go | 11 +- .../service/gateway/card_payout_store_test.go | 6 +- .../service/gateway/card_processor.go | 23 +-- .../service/gateway/card_processor_test.go | 31 +-- .../gateway/card_tokenize_validation_test.go | 4 +- .../internal/service/gateway/connector.go | 4 +- .../service/gateway/payout_execution_mode.go | 7 +- .../gateway/payout_failure_policy_test.go | 3 - .../mntx/internal/service/gateway/service.go | 2 +- .../service/gateway/testhelpers_test.go | 6 +- .../service/gateway/transfer_notifications.go | 2 +- .../mntx/internal/service/monetix/sender.go | 8 +- .../internal/service/monetix/sender_test.go | 15 +- api/gateway/tgsettle/.golangci.yml | 47 +++++ .../service/gateway/confirmation_flow.go | 10 +- .../internal/service/gateway/connector.go | 27 ++- .../service/gateway/outbox_reliable.go | 6 +- .../internal/service/gateway/service.go | 27 ++- .../internal/service/gateway/service_test.go | 10 +- .../service/gateway/transfer_notifications.go | 2 +- .../internal/service/treasury/bot/router.go | 16 +- .../service/treasury/bot/router_test.go | 5 + .../service/treasury/ledger/client.go | 2 +- .../treasury/ledger/discovery_client.go | 6 +- .../internal/service/treasury/module.go | 4 +- .../internal/service/treasury/scheduler.go | 26 ++- .../internal/service/treasury/service.go | 10 +- .../tgsettle/storage/mongo/store/payments.go | 2 + .../mongo/store/pending_confirmations.go | 2 + .../storage/mongo/store/treasury_requests.go | 2 + .../mongo/store/treasury_telegram_users.go | 2 + api/gateway/tron/.golangci.yml | 47 +++++ api/gateway/tron/client/client.go | 2 + .../tron/internal/keymanager/vault/manager.go | 2 +- .../internal/server/internal/serverimp.go | 1 + .../gateway/commands/transfer/gas_topup.go | 1 + .../service/gateway/commands/wallet/create.go | 2 +- .../internal/service/gateway/connector.go | 51 +++-- .../gateway/driver/tron/confirmation.go | 9 +- .../service/gateway/driver/tron/transfer.go | 10 +- .../service/gateway/outbox_reliable.go | 6 +- .../service/gateway/rpcclient/clients.go | 4 +- .../tron/internal/service/gateway/service.go | 4 +- .../internal/service/gateway/service_test.go | 7 +- .../service/gateway/transfer_execution.go | 16 +- .../service/gateway/transfer_notifications.go | 14 +- .../service/gateway/tronclient/client.go | 2 +- api/gateway/tron/shared/hex.go | 7 +- .../tron/storage/mongo/store/wallets.go | 1 - api/ledger/.golangci.yml | 47 +++++ api/ledger/client/client.go | 2 +- api/ledger/client/client_test.go | 14 +- api/ledger/internal/model/account.go | 4 +- api/ledger/internal/model/ownership.go | 3 + api/ledger/internal/model/party.go | 3 + .../internal/service/ledger/account_status.go | 5 +- .../internal/service/ledger/accounts.go | 2 +- .../internal/service/ledger/accounts_test.go | 2 +- .../internal/service/ledger/connector.go | 19 +- .../ledger/external_operations_test.go | 2 +- .../internal/service/ledger/invariant.go | 4 +- .../service/ledger/outbox_reliable.go | 9 +- api/ledger/internal/service/ledger/posting.go | 5 +- .../internal/service/ledger/posting_debit.go | 5 +- .../service/ledger/posting_external.go | 9 +- .../internal/service/ledger/posting_fx.go | 9 +- .../service/ledger/posting_transfer.go | 5 +- api/ledger/internal/service/ledger/queries.go | 9 +- api/ledger/internal/service/ledger/service.go | 4 +- .../mongo/store/testing_helpers_test.go | 6 +- api/notification/.golangci.yml | 47 +++++ .../internal/server/amplitude/nsent.go | 16 -- .../notificationimp/confirmation_request.go | 4 +- .../notificationimp/mail/internal/mailimp.go | 5 +- .../server/notificationimp/notification.go | 2 +- .../notificationimp/notification_test.go | 1 - .../server/notificationimp/telegram/client.go | 12 +- .../server/notificationimp/webhook.go | 2 +- api/notification/main.go | 4 +- api/payments/methods/.golangci.yml | 47 +++++ api/payments/methods/client/client.go | 56 ++++-- api/payments/orchestrator/.golangci.yml | 47 +++++ api/payments/orchestrator/client/client.go | 2 +- .../internal/server/internal/discovery.go | 2 +- .../server/internal/discovery_clients.go | 11 +- .../server/internal/discovery_wrappers.go | 6 +- .../service/orchestrationv2/agg/service.go | 2 +- .../orchestrationv2/agg/service_test.go | 1 - .../orchestrationv2/oobs/audit_store.go | 4 +- .../service/orchestrationv2/opagg/clone.go | 2 +- .../service/orchestrationv2/pquery/service.go | 6 +- .../service/orchestrationv2/prepo/document.go | 6 +- .../orchestrationv2/prepo/mongo_store.go | 9 +- .../service/orchestrationv2/prepo/service.go | 2 +- .../service/orchestrationv2/prmap/service.go | 2 +- .../orchestrationv2/psvc/aggregate_state.go | 4 +- .../orchestrationv2/psvc/execute_batch.go | 5 +- .../service/orchestrationv2/psvc/query.go | 4 +- .../orchestrationv2/psvc/request_helpers.go | 6 +- .../qsnap/resolve_errors_test.go | 1 - .../service/orchestrationv2/qsnap/service.go | 4 +- .../orchestrationv2/reqval/validator_test.go | 1 - .../xplan/compile_policy_test.go | 1 - .../xplan/test_helpers_test.go | 11 +- .../orchestrator/crypto_executor_test.go | 4 +- api/payments/quotation/.golangci.yml | 47 +++++ .../server/internal/discovery_clients.go | 2 +- .../internal/service/plan/builder.go | 2 +- .../internal/service/plan/helpers.go | 8 +- .../funding_gate_builder.go | 2 +- .../funding_profile_resolver_static.go | 4 +- .../graph_path_finder/service_test.go | 13 +- .../internal/service/quotation/helpers.go | 8 +- .../service/quotation/internal_helpers.go | 4 +- .../quotation_service_v2/service_e2e_test.go | 6 +- .../managed_wallet_network_test.go | 4 +- .../quote_computation_service/planner.go | 4 +- .../service/quotation/quote_engine.go | 10 +- .../transfer_intent_hydrator/hydrator.go | 6 +- api/payments/storage/.golangci.yml | 47 +++++ api/pkg/.golangci.yml | 47 +++++ api/pkg/api/routers/gsresponse/response.go | 2 +- .../api/routers/internal/grpcimp/router.go | 22 +-- .../routers/internal/grpcimp/router_test.go | 2 +- api/pkg/auth/dbimp.go | 30 +-- api/pkg/auth/dbimpab.go | 22 +-- api/pkg/auth/dbimpab_test.go | 7 +- api/pkg/auth/internal/casbin/action.go | 23 --- api/pkg/auth/internal/casbin/config/config.go | 5 +- api/pkg/auth/internal/native/db/policies.go | 6 +- api/pkg/auth/internal/native/db/roles.go | 6 +- api/pkg/auth/internal/native/enforcer_test.go | 22 +-- api/pkg/db/connection.go | 6 - api/pkg/db/internal/mongo/accountdb/db.go | 2 +- .../internal/mongo/chainassetsdb/resolve.go | 2 +- .../mongo/repositoryimp/builderimp/func.go | 2 +- .../mongo/repositoryimp/repository.go | 16 +- .../db/internal/mongo/tseriesimp/tseries.go | 7 +- .../internal/mongo/verificationimp/consume.go | 12 +- .../internal/mongo/verificationimp/create.go | 22 +-- .../verificationimp/verification_test.go | 8 +- api/pkg/discovery/messages.go | 4 +- api/pkg/discovery/service.go | 1 - .../messaging/internal/natsb/broker_test.go | 4 + .../notifications/account/notification.go | 2 +- .../notifications/account/password_reset.go | 2 +- .../confirmation/notification.go | 2 +- .../confirmations/notification.go | 6 +- .../notification/notification.go | 2 +- .../internal/notifications/object/object.go | 2 +- .../paymentgateway/notification.go | 4 +- .../paymentorchestrator/notification.go | 2 +- .../notifications/site/notification.go | 2 +- .../notifications/telegram/notification.go | 6 +- api/pkg/model/chainasset_test.go | 3 +- api/pkg/model/internal/notificationevent.go | 2 +- api/pkg/model/notificationevent.go | 2 +- api/pkg/mservice/services.go | 4 +- api/pkg/mutil/fr/fr.go | 1 + api/pkg/mutil/http/http.go | 6 +- api/pkg/mutil/reorder/reorder.go | 2 +- api/pkg/server/grpcapp/app.go | 10 +- api/pkg/server/internal/server.go | 8 +- api/pkg/vault/kv/service.go | 1 + 287 files changed, 2089 insertions(+), 1550 deletions(-) create mode 100644 api/edge/bff/.golangci.yml create mode 100644 api/edge/callbacks/.golangci.yml create mode 100644 api/fx/oracle/.golangci.yml create mode 100644 api/fx/storage/.golangci.yml create mode 100644 api/gateway/aurora/.golangci.yml create mode 100644 api/gateway/chain/.golangci.yml create mode 100644 api/gateway/chsettle/.golangci.yml create mode 100644 api/gateway/common/.golangci.yml create mode 100644 api/gateway/mntx/.golangci.yml create mode 100644 api/gateway/tgsettle/.golangci.yml create mode 100644 api/gateway/tron/.golangci.yml create mode 100644 api/ledger/.golangci.yml create mode 100644 api/notification/.golangci.yml delete mode 100644 api/notification/internal/server/amplitude/nsent.go create mode 100644 api/payments/methods/.golangci.yml create mode 100644 api/payments/orchestrator/.golangci.yml create mode 100644 api/payments/quotation/.golangci.yml create mode 100644 api/payments/storage/.golangci.yml create mode 100644 api/pkg/.golangci.yml delete mode 100644 api/pkg/auth/internal/casbin/action.go diff --git a/.gitignore b/.gitignore index 5c9ac87c..7123c9b5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ generate_protos.sh update_dep.sh test.sh .vscode/ + .gocache/ +.golangci-cache/ .cache/ .claude/ diff --git a/Makefile b/Makefile index d217e5c0..75689c95 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,12 @@ # Sendico Development Environment - Makefile # Docker Compose + Makefile build system -.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend backend-up backend-down backend-rebuild +.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend lint-api backend-up backend-down backend-rebuild COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev SERVICE ?= +API_GOCACHE ?= $(CURDIR)/.gocache +API_GOLANGCI_LINT_CACHE ?= $(CURDIR)/.golangci-cache BACKEND_SERVICES := \ dev-discovery \ dev-fx-oracle \ @@ -76,6 +78,7 @@ help: @echo " make test Run all tests (API + frontend)" @echo " make test-api Run Go API tests only" @echo " make test-frontend Run Flutter tests only" + @echo " make lint-api Run golangci-lint across all API Go modules" @echo " make health Check service health" @echo "" @echo "Examples:" @@ -374,3 +377,18 @@ test-frontend: @cd frontend/pshared && flutter test @cd frontend/pweb && flutter test @echo "$(GREEN)✅ All frontend tests passed$(NC)" + +# Run Go API linting +lint-api: + @echo "$(GREEN)Running API linting...$(NC)" + @mkdir -p "$(API_GOCACHE)" "$(API_GOLANGCI_LINT_CACHE)" + @failed=""; \ + for dir in $$(find api -name go.mod -exec dirname {} \;); do \ + echo "Linting $$dir..."; \ + (cd "$$dir" && GOCACHE="$(API_GOCACHE)" GOLANGCI_LINT_CACHE="$(API_GOLANGCI_LINT_CACHE)" golangci-lint run --allow-serial-runners --allow-parallel-runners ./...) || failed="$$failed $$dir"; \ + done; \ + if [ -n "$$failed" ]; then \ + echo "$(YELLOW)Lint failed:$$failed$(NC)"; \ + exit 1; \ + fi + @echo "$(GREEN)✅ All API lint checks passed$(NC)" diff --git a/api/billing/documents/.golangci.yml b/api/billing/documents/.golangci.yml index d292eb83..60a80daf 100644 --- a/api/billing/documents/.golangci.yml +++ b/api/billing/documents/.golangci.yml @@ -1,196 +1,47 @@ -# See the dedicated "version" documentation section. version: "2" linters: - # Default set of linters. - # The value can be: - # - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default - # - `all`: enables all linters by default. - # - `none`: disables all linters by default. - # - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`). - # Default: standard - default: all - # Enable specific linter. + default: none enable: - - arangolint - - asasalint - - asciicheck - - bidichk - bodyclose - canonicalheader - - containedctx - - contextcheck - copyloopvar - - cyclop - - decorder - - dogsled - - dupl - - dupword - durationcheck - - embeddedstructfieldcheck - - err113 - errcheck - errchkjson - errname - errorlint - - exhaustive - - exptostd - - fatcontext - - forbidigo - - forcetypeassert - - funcorder - - funlen - - ginkgolinter - - gocheckcompilerdirectives - - gochecknoglobals - - gochecknoinits - - gochecksumtype - - gocognit - - goconst - - gocritic - - gocyclo - - godoclint - - godot - - godox - - goheader - - gomodguard - - goprintffuncname - gosec - - gosmopolitan - govet - - grouper - - iface - - importas - - inamedparam - ineffassign - - interfacebloat - - intrange - - iotamixing - - ireturn - - lll - - loggercheck - - maintidx - - makezero - - mirror - - misspell - - mnd - - modernize - - musttag - - nakedret - - nestif - nilerr - nilnesserr - nilnil - - nlreturn - noctx - - noinlineerr - - nolintlint - - nonamedreturns - - nosprintfhostport - - paralleltest - - perfsprint - - prealloc - - predeclared - - promlinter - - protogetter - - reassign - - recvcheck - - revive - rowserrcheck - - sloglint - - spancheck - sqlclosecheck - staticcheck - - tagalign - - tagliatelle - - testableexamples - - testifylint - - testpackage - - thelper - - tparallel - unconvert - - unparam - - unqueryvet - - unused - - usestdlibvars - - usetesting - - varnamelen - wastedassign - - whitespace - - wsl_v5 - - zerologlint - # Disable specific linters. - disable: + disable: - depguard - exhaustruct - gochecknoglobals + - gochecknoinits - gomoddirectives - - wsl - wrapcheck - # All available settings of specific linters. - # See the dedicated "linters.settings" documentation section. - settings: - wsl_v5: - allow-first-in-block: true - allow-whole-block: false - branch-max-lines: 2 - - # Defines a set of rules to ignore issues. - # It does not skip the analysis, and so does not ignore "typecheck" errors. - exclusions: - # Mode of the generated files analysis. - # - # - `strict`: sources are excluded by strictly following the Go generated file convention. - # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` - # This line must appear before the first non-comment, non-blank text in the file. - # https://go.dev/s/generatedcode - # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. - # - `disable`: disable the generated files exclusion. - # - # Default: strict - generated: lax - # Log a warning if an exclusion rule is unused. - # Default: false - warn-unused: true - # Predefined exclusion rules. - # Default: [] - presets: - - comments - - std-error-handling - - common-false-positives - - legacy - # Excluding configuration per-path, per-linter, per-text and per-source. - rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - funlen - - gocyclo - - errcheck - - dupl - - gosec - # Run some linter only for test files by excluding its issues for everything else. - - path-except: _test\.go - linters: - - forbidigo - # Exclude known linters from partially hard-vendored code, - # which is impossible to exclude via `nolint` comments. - # `/` will be replaced by the current OS file path separator to properly work on Windows. - - path: internal/hmac/ - text: "weak cryptographic primitive" - linters: - - gosec - # Exclude some `staticcheck` messages. - - linters: - - staticcheck - text: "SA9003:" - # Exclude `lll` issues for long lines with `go:generate`. - - linters: - - lll - source: "^//go:generate " - # Which file paths to exclude: they will be analyzed, but issues from them won't be reported. - # "/" will be replaced by the current OS file path separator to properly work on Windows. - # Default: [] - paths: [] - # Which file paths to not exclude. - # Default: [] - paths-except: [] + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/billing/documents/internal/docstore/local.go b/api/billing/documents/internal/docstore/local.go index b1bc79c7..f7bddae9 100644 --- a/api/billing/documents/internal/docstore/local.go +++ b/api/billing/documents/internal/docstore/local.go @@ -2,6 +2,7 @@ package docstore import ( "context" + "fmt" "os" "path/filepath" "strings" @@ -36,8 +37,12 @@ func (s *LocalStore) Save(ctx context.Context, key string, data []byte) error { return err } - path := filepath.Join(s.rootPath, filepath.Clean(key)) - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + path, err := resolveStoragePath(s.rootPath, key) + if err != nil { + return err + } + + if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { s.logger.Warn("Failed to create document directory", zap.Error(err), zap.String("path", path)) return err @@ -56,8 +61,12 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) { return nil, err } - path := filepath.Join(s.rootPath, filepath.Clean(key)) + path, err := resolveStoragePath(s.rootPath, key) + if err != nil { + return nil, err + } + //nolint:gosec // path is constrained by resolveStoragePath to stay within configured root. data, err := os.ReadFile(path) if err != nil { s.logger.Warn("Failed to read document file", zap.Error(err), zap.String("path", path)) @@ -69,3 +78,32 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) { } var _ Store = (*LocalStore)(nil) + +func resolveStoragePath(rootPath string, key string) (string, error) { + cleanKey := filepath.Clean(strings.TrimSpace(key)) + if cleanKey == "" || cleanKey == "." { + return "", merrors.InvalidArgument("docstore: key is required") + } + + if filepath.IsAbs(cleanKey) { + return "", merrors.InvalidArgument("docstore: absolute keys are not allowed") + } + + rootAbs, err := filepath.Abs(rootPath) + if err != nil { + return "", fmt.Errorf("resolve local store root: %w", err) + } + + path := filepath.Join(rootAbs, cleanKey) + pathAbs, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("resolve local store path: %w", err) + } + + prefix := rootAbs + string(filepath.Separator) + if pathAbs != rootAbs && !strings.HasPrefix(pathAbs, prefix) { + return "", merrors.InvalidArgument("docstore: key escapes root path") + } + + return pathAbs, nil +} diff --git a/api/billing/documents/internal/docstore/s3.go b/api/billing/documents/internal/docstore/s3.go index b962c231..b7663dce 100644 --- a/api/billing/documents/internal/docstore/s3.go +++ b/api/billing/documents/internal/docstore/s3.go @@ -124,7 +124,11 @@ func (s *S3Store) Load(ctx context.Context, key string) ([]byte, error) { return nil, err } - defer obj.Body.Close() + defer func() { + if closeErr := obj.Body.Close(); closeErr != nil { + s.logger.Warn("Failed to close document body", zap.Error(closeErr), zap.String("key", key)) + } + }() return io.ReadAll(obj.Body) } diff --git a/api/billing/documents/internal/service/documents/service.go b/api/billing/documents/internal/service/documents/service.go index 56ac179a..5257fc43 100644 --- a/api/billing/documents/internal/service/documents/service.go +++ b/api/billing/documents/internal/service/documents/service.go @@ -247,12 +247,6 @@ func (s *Service) startDiscoveryAnnouncer() { s.announcer.Start() } -type serviceError string - -func (e serviceError) Error() string { - return string(e) -} - func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, error) { blocks, err := s.template.Render(snapshot) if err != nil { diff --git a/api/billing/documents/internal/service/documents/service_test.go b/api/billing/documents/internal/service/documents/service_test.go index 195672be..bd102029 100644 --- a/api/billing/documents/internal/service/documents/service_test.go +++ b/api/billing/documents/internal/service/documents/service_test.go @@ -54,34 +54,6 @@ func (s *stubDocumentsStore) ListByPaymentRefs(_ context.Context, _ []string) ([ var _ storage.DocumentsStore = (*stubDocumentsStore)(nil) -type memDocStore struct { - data map[string][]byte - saveCount int - loadCount int -} - -func (m *memDocStore) Save(_ context.Context, key string, data []byte) error { - m.saveCount++ - copyData := make([]byte, len(data)) - copy(copyData, data) - m.data[key] = copyData - - return nil -} - -func (m *memDocStore) Load(_ context.Context, key string) ([]byte, error) { - m.loadCount++ - data := m.data[key] - copyData := make([]byte, len(data)) - copy(copyData, data) - - return copyData, nil -} - -func (m *memDocStore) Counts() (int, int) { - return m.saveCount, m.loadCount -} - type stubTemplate struct { blocks []renderer.Block calls int diff --git a/api/billing/documents/internal/service/documents/template.go b/api/billing/documents/internal/service/documents/template.go index 066d2a34..d7b9f791 100644 --- a/api/billing/documents/internal/service/documents/template.go +++ b/api/billing/documents/internal/service/documents/template.go @@ -24,6 +24,7 @@ type acceptanceTemplateData struct { } func newTemplateRenderer(path string) (*templateRenderer, error) { + //nolint:gosec // template file path is provided by trusted service configuration. data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read template: %w", err) diff --git a/api/billing/documents/renderer/renderer_test.go b/api/billing/documents/renderer/renderer_test.go index cc7bb5a7..b717a967 100644 --- a/api/billing/documents/renderer/renderer_test.go +++ b/api/billing/documents/renderer/renderer_test.go @@ -102,7 +102,7 @@ func encodeUTF16BE(text string, withBOM bool) []byte { } for _, v := range encoded { - out = append(out, byte(v>>8), byte(v)) + out = append(out, byte(v>>8), byte(v&0x00FF)) } return out diff --git a/api/billing/fees/.golangci.yml b/api/billing/fees/.golangci.yml index 73d4efc3..60a80daf 100644 --- a/api/billing/fees/.golangci.yml +++ b/api/billing/fees/.golangci.yml @@ -1,198 +1,47 @@ -# See the dedicated "version" documentation section. version: "2" linters: - # Default set of linters. - # The value can be: - # - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default - # - `all`: enables all linters by default. - # - `none`: disables all linters by default. - # - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`). - # Default: standard - default: all - # Enable specific linter. + default: none enable: - - arangolint - - asasalint - - asciicheck - - bidichk - bodyclose - canonicalheader - - containedctx - - contextcheck - copyloopvar - - cyclop - - decorder - - dogsled - - dupl - - dupword - durationcheck - - embeddedstructfieldcheck - - err113 - errcheck - errchkjson - errname - errorlint - - exhaustive - - exptostd - - fatcontext - - forbidigo - - forcetypeassert - - funcorder - - funlen - - ginkgolinter - - gocheckcompilerdirectives - - gochecknoglobals - - gochecknoinits - - gochecksumtype - - gocognit - - goconst - - gocritic - - gocyclo - - godoclint - - godot - - godox - - goheader - - gomodguard - - goprintffuncname - gosec - - gosmopolitan - govet - - grouper - - iface - - importas - - inamedparam - ineffassign - - interfacebloat - - intrange - - iotamixing - - ireturn - - lll - - loggercheck - - maintidx - - makezero - - mirror - - misspell - - mnd - - modernize - - musttag - - nakedret - - nestif - nilerr - nilnesserr - nilnil - - nlreturn - noctx - - noinlineerr - - nolintlint - - nonamedreturns - - nosprintfhostport - - paralleltest - - perfsprint - - prealloc - - predeclared - - promlinter - - protogetter - - reassign - - recvcheck - - revive - rowserrcheck - - sloglint - - spancheck - sqlclosecheck - staticcheck - - tagalign - - tagliatelle - - testableexamples - - testifylint - - testpackage - - thelper - - tparallel - unconvert - - unparam - - unqueryvet - - unused - - usestdlibvars - - usetesting - - varnamelen - wastedassign - - whitespace - - wsl_v5 - - zerologlint - # Disable specific linters. disable: - depguard - exhaustruct - gochecknoglobals + - gochecknoinits - gomoddirectives - - noinlineerr - - wsl - wrapcheck - # All available settings of specific linters. - # See the dedicated "linters.settings" documentation section. - settings: - wsl_v5: - allow-first-in-block: true - allow-whole-block: false - branch-max-lines: 2 - - # Defines a set of rules to ignore issues. - # It does not skip the analysis, and so does not ignore "typecheck" errors. - exclusions: - # Mode of the generated files analysis. - # - # - `strict`: sources are excluded by strictly following the Go generated file convention. - # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` - # This line must appear before the first non-comment, non-blank text in the file. - # https://go.dev/s/generatedcode - # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. - # - `disable`: disable the generated files exclusion. - # - # Default: strict - generated: lax - # Log a warning if an exclusion rule is unused. - # Default: false - warn-unused: true - # Predefined exclusion rules. - # Default: [] - presets: - - comments - - std-error-handling - - common-false-positives - - legacy - # Excluding configuration per-path, per-linter, per-text and per-source. - rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - cyclop - - funlen - - gocyclo - - errcheck - - dupl - - gosec - # Run some linter only for test files by excluding its issues for everything else. - - path-except: _test\.go - linters: - - forbidigo - # Exclude known linters from partially hard-vendored code, - # which is impossible to exclude via `nolint` comments. - # `/` will be replaced by the current OS file path separator to properly work on Windows. - - path: internal/hmac/ - text: "weak cryptographic primitive" - linters: - - gosec - # Exclude some `staticcheck` messages. - - linters: - - staticcheck - text: "SA9003:" - # Exclude `lll` issues for long lines with `go:generate`. - - linters: - - lll - source: "^//go:generate " - # Which file paths to exclude: they will be analyzed, but issues from them won't be reported. - # "/" will be replaced by the current OS file path separator to properly work on Windows. - # Default: [] - paths: [] - # Which file paths to not exclude. - # Default: [] - paths-except: [] + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/discovery/.golangci.yml b/api/discovery/.golangci.yml index d292eb83..60a80daf 100644 --- a/api/discovery/.golangci.yml +++ b/api/discovery/.golangci.yml @@ -1,196 +1,47 @@ -# See the dedicated "version" documentation section. version: "2" linters: - # Default set of linters. - # The value can be: - # - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default - # - `all`: enables all linters by default. - # - `none`: disables all linters by default. - # - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`). - # Default: standard - default: all - # Enable specific linter. + default: none enable: - - arangolint - - asasalint - - asciicheck - - bidichk - bodyclose - canonicalheader - - containedctx - - contextcheck - copyloopvar - - cyclop - - decorder - - dogsled - - dupl - - dupword - durationcheck - - embeddedstructfieldcheck - - err113 - errcheck - errchkjson - errname - errorlint - - exhaustive - - exptostd - - fatcontext - - forbidigo - - forcetypeassert - - funcorder - - funlen - - ginkgolinter - - gocheckcompilerdirectives - - gochecknoglobals - - gochecknoinits - - gochecksumtype - - gocognit - - goconst - - gocritic - - gocyclo - - godoclint - - godot - - godox - - goheader - - gomodguard - - goprintffuncname - gosec - - gosmopolitan - govet - - grouper - - iface - - importas - - inamedparam - ineffassign - - interfacebloat - - intrange - - iotamixing - - ireturn - - lll - - loggercheck - - maintidx - - makezero - - mirror - - misspell - - mnd - - modernize - - musttag - - nakedret - - nestif - nilerr - nilnesserr - nilnil - - nlreturn - noctx - - noinlineerr - - nolintlint - - nonamedreturns - - nosprintfhostport - - paralleltest - - perfsprint - - prealloc - - predeclared - - promlinter - - protogetter - - reassign - - recvcheck - - revive - rowserrcheck - - sloglint - - spancheck - sqlclosecheck - staticcheck - - tagalign - - tagliatelle - - testableexamples - - testifylint - - testpackage - - thelper - - tparallel - unconvert - - unparam - - unqueryvet - - unused - - usestdlibvars - - usetesting - - varnamelen - wastedassign - - whitespace - - wsl_v5 - - zerologlint - # Disable specific linters. - disable: + disable: - depguard - exhaustruct - gochecknoglobals + - gochecknoinits - gomoddirectives - - wsl - wrapcheck - # All available settings of specific linters. - # See the dedicated "linters.settings" documentation section. - settings: - wsl_v5: - allow-first-in-block: true - allow-whole-block: false - branch-max-lines: 2 - - # Defines a set of rules to ignore issues. - # It does not skip the analysis, and so does not ignore "typecheck" errors. - exclusions: - # Mode of the generated files analysis. - # - # - `strict`: sources are excluded by strictly following the Go generated file convention. - # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` - # This line must appear before the first non-comment, non-blank text in the file. - # https://go.dev/s/generatedcode - # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. - # - `disable`: disable the generated files exclusion. - # - # Default: strict - generated: lax - # Log a warning if an exclusion rule is unused. - # Default: false - warn-unused: true - # Predefined exclusion rules. - # Default: [] - presets: - - comments - - std-error-handling - - common-false-positives - - legacy - # Excluding configuration per-path, per-linter, per-text and per-source. - rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - funlen - - gocyclo - - errcheck - - dupl - - gosec - # Run some linter only for test files by excluding its issues for everything else. - - path-except: _test\.go - linters: - - forbidigo - # Exclude known linters from partially hard-vendored code, - # which is impossible to exclude via `nolint` comments. - # `/` will be replaced by the current OS file path separator to properly work on Windows. - - path: internal/hmac/ - text: "weak cryptographic primitive" - linters: - - gosec - # Exclude some `staticcheck` messages. - - linters: - - staticcheck - text: "SA9003:" - # Exclude `lll` issues for long lines with `go:generate`. - - linters: - - lll - source: "^//go:generate " - # Which file paths to exclude: they will be analyzed, but issues from them won't be reported. - # "/" will be replaced by the current OS file path separator to properly work on Windows. - # Default: [] - paths: [] - # Which file paths to not exclude. - # Default: [] - paths-except: [] + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/edge/bff/.golangci.yml b/api/edge/bff/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/edge/bff/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/edge/bff/interface/accountservice/internal/service.go b/api/edge/bff/interface/accountservice/internal/service.go index e00cfcc0..29caf834 100644 --- a/api/edge/bff/interface/accountservice/internal/service.go +++ b/api/edge/bff/interface/accountservice/internal/service.go @@ -159,7 +159,7 @@ func (s *service) VerifyAccount( token, err := s.vdb.Create( ctx, verification.NewLinkRequest(*acct.GetID(), model.PurposeAccountActivation, ""). - WithTTL(time.Duration(time.Hour*24)), + WithTTL(time.Hour * 24), ) if err != nil { s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct)) @@ -238,7 +238,7 @@ func (s *service) ResetPassword( return s.vdb.Create( ctx, verification.NewOTPRequest(*acct.GetID(), model.PurposePasswordReset, ""). - WithTTL(time.Duration(time.Hour*1)), + WithTTL(time.Hour), ) } @@ -250,7 +250,7 @@ func (s *service) UpdateLogin( return s.vdb.Create( ctx, verification.NewOTPRequest(*acct.GetID(), model.PurposeEmailChange, newLogin). - WithTTL(time.Duration(time.Hour*1)), + WithTTL(time.Hour), ) } diff --git a/api/edge/bff/interface/api/srequest/endpoint_union.go b/api/edge/bff/interface/api/srequest/endpoint_union.go index 4d0458c8..00b03182 100644 --- a/api/edge/bff/interface/api/srequest/endpoint_union.go +++ b/api/edge/bff/interface/api/srequest/endpoint_union.go @@ -164,6 +164,7 @@ func (e Endpoint) DecodeIBAN() (IBANEndpoint, error) { func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) { if old == nil { + //nolint:nilnil // Nil legacy endpoint means no endpoint provided. return nil, nil } @@ -202,6 +203,7 @@ func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint, error) { if new == nil { + //nolint:nilnil // Nil endpoint DTO means no endpoint provided. return nil, nil } diff --git a/api/edge/bff/interface/api/sresponse/payment.go b/api/edge/bff/interface/api/sresponse/payment.go index 0ec2559e..acdd44c7 100644 --- a/api/edge/bff/interface/api/sresponse/payment.go +++ b/api/edge/bff/interface/api/sresponse/payment.go @@ -344,7 +344,7 @@ func toPaymentOperation(step *orchestrationv2.StepExecution, quote *quotationv2. Amount: amount, ConvertedAmount: convertedAmount, OperationRef: operationRef, - Gateway: string(gateway), + Gateway: gateway, StartedAt: timestampAsTime(step.GetStartedAt()), CompletedAt: timestampAsTime(step.GetCompletedAt()), } @@ -456,9 +456,9 @@ func gatewayTypeFromInstanceID(raw string) mservice.Type { return "" } - switch mservice.Type(value) { + switch value { case mservice.ChainGateway, mservice.TronGateway, mservice.MntxGateway, mservice.PaymentGateway, mservice.TgSettle, mservice.Ledger: - return mservice.Type(value) + return value } switch { diff --git a/api/edge/bff/interface/model/token.go b/api/edge/bff/interface/model/token.go index 79154ba9..495ea8be 100644 --- a/api/edge/bff/interface/model/token.go +++ b/api/edge/bff/interface/model/token.go @@ -110,7 +110,7 @@ func Account2ClaimsForClient(a *model.Account, expiration int, clientID string) paramNameName: t.Name, paramNameLocale: t.Locale, paramNameClientID: t.ClientID, - paramNameExpiration: int64(t.Expiration.Unix()), + paramNameExpiration: t.Expiration.Unix(), paramNamePending: t.Pending, } } diff --git a/api/edge/bff/internal/api/discovery_resolver.go b/api/edge/bff/internal/api/discovery_resolver.go index 93952478..0e0e2d5a 100644 --- a/api/edge/bff/internal/api/discovery_resolver.go +++ b/api/edge/bff/internal/api/discovery_resolver.go @@ -26,11 +26,11 @@ const ( var ( ledgerDiscoveryServiceNames = []string{ "LEDGER", - string(mservice.Ledger), + mservice.Ledger, } paymentOrchestratorDiscoveryServiceNames = []string{ "PAYMENTS_ORCHESTRATOR", - string(mservice.PaymentOrchestrator), + mservice.PaymentOrchestrator, } paymentQuotationDiscoveryServiceNames = []string{ "PAYMENTS_QUOTATION", @@ -41,7 +41,7 @@ var ( paymentMethodsDiscoveryServiceNames = []string{ "PAYMENTS_METHODS", "PAYMENT_METHODS", - string(mservice.PaymentMethods), + mservice.PaymentMethods, } ) @@ -339,13 +339,13 @@ func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) { raw = strings.TrimSpace(raw) if raw == "" { - return discoveryEndpoint{}, fmt.Errorf("Invoke uri is empty") + return discoveryEndpoint{}, fmt.Errorf("invoke uri is empty") } // Without a scheme we expect a plain host:port target. if !strings.Contains(raw, "://") { if _, _, err := net.SplitHostPort(raw); err != nil { - return discoveryEndpoint{}, fmt.Errorf("Invoke uri must include host:port: %w", err) + return discoveryEndpoint{}, fmt.Errorf("invoke uri must include host:port: %w", err) } return discoveryEndpoint{ address: raw, @@ -363,7 +363,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) { case "grpc": address := strings.TrimSpace(parsed.Host) if _, _, splitErr := net.SplitHostPort(address); splitErr != nil { - return discoveryEndpoint{}, fmt.Errorf("Grpc invoke uri must include host:port: %w", splitErr) + return discoveryEndpoint{}, fmt.Errorf("grpc invoke uri must include host:port: %w", splitErr) } return discoveryEndpoint{ address: address, @@ -373,7 +373,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) { case "grpcs": address := strings.TrimSpace(parsed.Host) if _, _, splitErr := net.SplitHostPort(address); splitErr != nil { - return discoveryEndpoint{}, fmt.Errorf("Grpcs invoke uri must include host:port: %w", splitErr) + return discoveryEndpoint{}, fmt.Errorf("grpcs invoke uri must include host:port: %w", splitErr) } return discoveryEndpoint{ address: address, @@ -388,7 +388,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) { raw: raw, }, nil default: - return discoveryEndpoint{}, fmt.Errorf("Unsupported invoke uri scheme: %s", parsed.Scheme) + return discoveryEndpoint{}, fmt.Errorf("unsupported invoke uri scheme: %s", parsed.Scheme) } } diff --git a/api/edge/bff/internal/api/ws/dispimp.go b/api/edge/bff/internal/api/ws/dispimp.go index fb2b20f9..ea08f4c1 100644 --- a/api/edge/bff/internal/api/ws/dispimp.go +++ b/api/edge/bff/internal/api/ws/dispimp.go @@ -43,6 +43,7 @@ func (d *DispatcherImpl) dispatchMessage(ctx context.Context, conn *websocket.Co } func (d *DispatcherImpl) handle(w http.ResponseWriter, r *http.Request) { + //nolint:contextcheck // websocket.Handler callback signature does not carry request context. websocket.Handler(func(conn *websocket.Conn) { ctx, cancel := context.WithTimeout(r.Context(), time.Duration(d.timeout)*time.Second) defer cancel() diff --git a/api/edge/bff/internal/mutil/param/getter.go b/api/edge/bff/internal/mutil/param/getter.go index d8eba536..0ff42590 100644 --- a/api/edge/bff/internal/mutil/param/getter.go +++ b/api/edge/bff/internal/mutil/param/getter.go @@ -71,6 +71,7 @@ func GetOptionalParam[T any](logger mlogger.Logger, r *http.Request, key string, vals := r.URL.Query() s := vals.Get(key) if s == "" { + //nolint:nilnil // Missing optional query parameter is represented as (nil, nil). return nil, nil } diff --git a/api/edge/bff/internal/mutil/param/getter_test.go b/api/edge/bff/internal/mutil/param/getter_test.go index ea26f781..61b6e8e6 100644 --- a/api/edge/bff/internal/mutil/param/getter_test.go +++ b/api/edge/bff/internal/mutil/param/getter_test.go @@ -1,17 +1,17 @@ package mutil import ( + "context" "net/http" "testing" - "github.com/tech/sendico/pkg/mlogger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func TestGetOptionalBoolParam(t *testing.T) { - logger := mlogger.Logger(zap.NewNop()) + logger := zap.NewNop() tests := []struct { name string @@ -47,7 +47,7 @@ func TestGetOptionalBoolParam(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil) + req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil) require.NoError(t, err) result, err := GetOptionalBoolParam(logger, req, "param") @@ -69,7 +69,7 @@ func TestGetOptionalBoolParam(t *testing.T) { } func TestGetOptionalInt64Param(t *testing.T) { - logger := mlogger.Logger(zap.NewNop()) + logger := zap.NewNop() tests := []struct { name string @@ -111,7 +111,7 @@ func TestGetOptionalInt64Param(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil) + req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil) require.NoError(t, err) result, err := GetOptionalInt64Param(logger, req, "param") diff --git a/api/edge/bff/internal/server/accountapiimp/delete.go b/api/edge/bff/internal/server/accountapiimp/delete.go index 94239332..b1f7523f 100644 --- a/api/edge/bff/internal/server/accountapiimp/delete.go +++ b/api/edge/bff/internal/server/accountapiimp/delete.go @@ -114,7 +114,7 @@ func (a *AccountAPI) deleteAll(r *http.Request, account *model.Account, token *s a.logger.Warn("Failed to delete all data", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account)) return nil, err } - return nil, nil + return struct{}{}, nil }); err != nil { a.logger.Warn("Failed to execute delete transaction", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account)) return response.Auto(a.logger, a.Name(), err) diff --git a/api/edge/bff/internal/server/accountapiimp/password.go b/api/edge/bff/internal/server/accountapiimp/password.go index b429621b..fa979e55 100644 --- a/api/edge/bff/internal/server/accountapiimp/password.go +++ b/api/edge/bff/internal/server/accountapiimp/password.go @@ -192,5 +192,5 @@ func (a *AccountAPI) resetPasswordTransactionBody(ctx context.Context, user *mod // Don't fail the transaction if token revocation fails, but log it } - return nil, nil + return struct{}{}, nil } diff --git a/api/edge/bff/internal/server/accountapiimp/password_test.go b/api/edge/bff/internal/server/accountapiimp/password_test.go index 9ade83ab..414b49ec 100644 --- a/api/edge/bff/internal/server/accountapiimp/password_test.go +++ b/api/edge/bff/internal/server/accountapiimp/password_test.go @@ -133,12 +133,7 @@ func TestPasswordValidationLogic(t *testing.T) { for _, password := range invalidPasswords { t.Run(password, func(t *testing.T) { // Test that invalid passwords fail at least one requirement - isValid := true - - // Check length - if len(password) < 8 { - isValid = false - } + isValid := len(password) >= 8 // Check for digit hasDigit := false diff --git a/api/edge/bff/internal/server/accountapiimp/signup.go b/api/edge/bff/internal/server/accountapiimp/signup.go index 8bc3d3aa..67286787 100644 --- a/api/edge/bff/internal/server/accountapiimp/signup.go +++ b/api/edge/bff/internal/server/accountapiimp/signup.go @@ -276,7 +276,7 @@ func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef bs for resource, granted := range required { if !granted { - a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", string(resource))) + a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", resource)) } } diff --git a/api/edge/bff/internal/server/accountapiimp/signup_integration_test.go b/api/edge/bff/internal/server/accountapiimp/signup_integration_test.go index fbe3acbc..9f45c7fb 100644 --- a/api/edge/bff/internal/server/accountapiimp/signup_integration_test.go +++ b/api/edge/bff/internal/server/accountapiimp/signup_integration_test.go @@ -126,7 +126,7 @@ func TestSignupHTTPSerialization(t *testing.T) { require.NoError(t, err) // Create HTTP request - req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBuffer(reqBody)) + req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBuffer(reqBody)) req.Header.Set("Content-Type", "application/json") // Parse the request body @@ -162,7 +162,7 @@ func TestSignupHTTPSerialization(t *testing.T) { t.Run("InvalidJSONRequest", func(t *testing.T) { invalidJSON := `{"account": {"login": "test@example.com", "password": "invalid json structure` - req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON)) + req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON)) req.Header.Set("Content-Type", "application/json") var parsedRequest srequest.Signup diff --git a/api/edge/bff/internal/server/callbacksimp/create.go b/api/edge/bff/internal/server/callbacksimp/create.go index 6dff4e6a..f21e72f4 100644 --- a/api/edge/bff/internal/server/callbacksimp/create.go +++ b/api/edge/bff/internal/server/callbacksimp/create.go @@ -37,7 +37,7 @@ func (a *CallbacksAPI) create(r *http.Request, account *model.Account, accessTok if err := a.applySigningSecretMutation(ctx, *account.GetID(), *callback.GetID(), mutation); err != nil { return nil, err } - return nil, nil + return struct{}{}, nil }); err != nil { a.Logger.Warn("Failed to create callback transaction", zap.Error(err)) return response.Auto(a.Logger, a.Name(), err) diff --git a/api/edge/bff/internal/server/callbacksimp/update.go b/api/edge/bff/internal/server/callbacksimp/update.go index 4ee3190e..8874a4f6 100644 --- a/api/edge/bff/internal/server/callbacksimp/update.go +++ b/api/edge/bff/internal/server/callbacksimp/update.go @@ -49,7 +49,7 @@ func (a *CallbacksAPI) update(r *http.Request, account *model.Account, accessTok if err := a.applySigningSecretMutation(ctx, *account.GetID(), callbackRef, mutation); err != nil { return nil, err } - return nil, nil + return struct{}{}, nil }); err != nil { a.Logger.Warn("Failed to update callback transaction", zap.Error(err)) return response.Auto(a.Logger, a.Name(), err) diff --git a/api/edge/bff/internal/server/fileserviceimp/storage/localfs.go b/api/edge/bff/internal/server/fileserviceimp/storage/localfs.go index 96239d76..40a2253b 100644 --- a/api/edge/bff/internal/server/fileserviceimp/storage/localfs.go +++ b/api/edge/bff/internal/server/fileserviceimp/storage/localfs.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/domainprovider" @@ -34,7 +35,10 @@ func (storage *LocalStorage) Delete(ctx context.Context, objID string) error { default: } - filePath := filepath.Join(storage.storageDir, objID) + filePath, err := storage.resolvePath(objID) + if err != nil { + return err + } if err := os.Remove(filePath); err != nil { if os.IsNotExist(err) { storage.logger.Debug("File not found", zap.String("obj_ref", objID)) @@ -54,7 +58,11 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str default: } - filePath := filepath.Join(storage.storageDir, objID) + filePath, err := storage.resolvePath(objID) + if err != nil { + return "", err + } + //nolint:gosec // File path is resolved and constrained to storage root. dst, err := os.Create(filePath) if err != nil { storage.logger.Warn("Error occurred while creating file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objID)) @@ -78,7 +86,9 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str } case <-ctx.Done(): // Context was cancelled, clean up the partial file - os.Remove(filePath) + if removeErr := os.Remove(filePath); removeErr != nil && !os.IsNotExist(removeErr) { + storage.logger.Warn("Failed to remove partially written file", zap.Error(removeErr), zap.String("obj_ref", objID)) + } return "", ctx.Err() } @@ -93,7 +103,10 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle default: } - filePath := filepath.Join(storage.storageDir, objRef) + filePath, err := storage.resolvePath(objRef) + if err != nil { + return response.Internal(storage.logger, storage.service, err) + } if _, err := os.Stat(filePath); err != nil { storage.logger.Warn("Failed to access file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objRef)) return response.Internal(storage.logger, storage.service, err) @@ -117,7 +130,7 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle func ensureDir(dirName string) error { info, err := os.Stat(dirName) if os.IsNotExist(err) { - return os.MkdirAll(dirName, 0o755) + return os.MkdirAll(dirName, 0o750) } if err != nil { return err @@ -128,6 +141,24 @@ func ensureDir(dirName string) error { return nil } +func (storage *LocalStorage) resolvePath(objID string) (string, error) { + objID = strings.TrimSpace(objID) + if objID == "" { + return "", merrors.InvalidArgument("obj_ref is required", "obj_ref") + } + + filePath := filepath.Join(storage.storageDir, objID) + relPath, err := filepath.Rel(storage.storageDir, filePath) + if err != nil { + return "", merrors.InternalWrap(err, "failed to resolve local file path") + } + if relPath == "." || strings.HasPrefix(relPath, "..") { + return "", merrors.InvalidArgument("obj_ref is invalid", "obj_ref") + } + + return filePath, nil +} + func CreateLocalFileStorage(logger mlogger.Logger, service mservice.Type, directory, subDir string, dp domainprovider.DomainProvider, cfg config.LocalFSSConfig) (*LocalStorage, error) { dir := filepath.Join(cfg.RootPath, directory) if err := ensureDir(dir); err != nil { diff --git a/api/edge/bff/internal/server/fileserviceimp/storage/localfs_test.go b/api/edge/bff/internal/server/fileserviceimp/storage/localfs_test.go index da1a13bd..761d4fc2 100644 --- a/api/edge/bff/internal/server/fileserviceimp/storage/localfs_test.go +++ b/api/edge/bff/internal/server/fileserviceimp/storage/localfs_test.go @@ -55,7 +55,7 @@ func setupTestStorage(t *testing.T) (*LocalStorage, string, func()) { // Return cleanup function cleanup := func() { - os.RemoveAll(tempDir) + require.NoError(t, os.RemoveAll(tempDir)) } return storage, tempDir, cleanup @@ -81,7 +81,7 @@ func setupBenchmarkStorage(b *testing.B) (*LocalStorage, string, func()) { // Return cleanup function cleanup := func() { - os.RemoveAll(tempDir) + require.NoError(b, os.RemoveAll(tempDir)) } return storage, tempDir, cleanup @@ -138,6 +138,7 @@ func TestLocalStorage_Save(t *testing.T) { // Verify file was actually saved filePath := filepath.Join(tempDir, tt.objID) + //nolint:gosec // Test-controlled path inside temporary directory. content, err := os.ReadFile(filePath) assert.NoError(t, err) assert.Equal(t, tt.content, string(content)) @@ -186,7 +187,7 @@ func TestLocalStorage_Delete(t *testing.T) { // Create a test file testFile := filepath.Join(tempDir, "test.txt") - err := os.WriteFile(testFile, []byte("test content"), 0o644) + err := os.WriteFile(testFile, []byte("test content"), 0o600) require.NoError(t, err) tests := []struct { @@ -232,7 +233,7 @@ func TestLocalStorage_Delete_ContextCancellation(t *testing.T) { // Create a test file testFile := filepath.Join(tempDir, "test.txt") - err := os.WriteFile(testFile, []byte("test content"), 0o644) + err := os.WriteFile(testFile, []byte("test content"), 0o600) require.NoError(t, err) // Create a context that's already cancelled @@ -256,7 +257,7 @@ func TestLocalStorage_Get(t *testing.T) { // Create a test file testContent := "test file content" testFile := filepath.Join(tempDir, "test.txt") - err := os.WriteFile(testFile, []byte(testContent), 0o644) + err := os.WriteFile(testFile, []byte(testContent), 0o600) require.NoError(t, err) tests := []struct { @@ -285,7 +286,7 @@ func TestLocalStorage_Get(t *testing.T) { handler := storage.Get(ctx, tt.objID) // Create test request - req := httptest.NewRequest("GET", "/files/"+tt.objID, nil) + req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/"+tt.objID, nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) @@ -304,7 +305,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) { // Create a test file testFile := filepath.Join(tempDir, "test.txt") - err := os.WriteFile(testFile, []byte("test content"), 0o644) + err := os.WriteFile(testFile, []byte("test content"), 0o600) require.NoError(t, err) // Create a context that's already cancelled @@ -314,7 +315,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) { handler := storage.Get(ctx, "test.txt") // Create test request - req := httptest.NewRequest("GET", "/files/test.txt", nil) + req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) @@ -328,14 +329,14 @@ func TestLocalStorage_Get_RequestContextCancellation(t *testing.T) { // Create a test file testFile := filepath.Join(tempDir, "test.txt") - err := os.WriteFile(testFile, []byte("test content"), 0o644) + err := os.WriteFile(testFile, []byte("test content"), 0o600) require.NoError(t, err) ctx := context.Background() handler := storage.Get(ctx, "test.txt") // Create test request with cancelled context - req := httptest.NewRequest("GET", "/files/test.txt", nil) + req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil) reqCtx, cancel := context.WithCancel(req.Context()) req = req.WithContext(reqCtx) cancel() // Cancel the request context @@ -352,7 +353,9 @@ func TestCreateLocalFileStorage(t *testing.T) { // Create temporary directory for testing tempDir, err := os.MkdirTemp("", "storage_test") require.NoError(t, err) - defer os.RemoveAll(tempDir) + defer func() { + require.NoError(t, os.RemoveAll(tempDir)) + }() logger := zap.NewNop() cfg := config.LocalFSSConfig{ @@ -372,10 +375,12 @@ func TestCreateLocalFileStorage_InvalidPath(t *testing.T) { // Build a deterministic failure case: the target path already exists as a file. tempDir, err := os.MkdirTemp("", "storage_invalid_path_test") require.NoError(t, err) - defer os.RemoveAll(tempDir) + defer func() { + require.NoError(t, os.RemoveAll(tempDir)) + }() fileAtTargetPath := filepath.Join(tempDir, "test") - err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o644) + err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o600) require.NoError(t, err) logger := zap.NewNop() @@ -426,7 +431,7 @@ func TestLocalStorage_ConcurrentOperations(t *testing.T) { // Create files to delete for i := 0; i < 5; i++ { filePath := filepath.Join(tempDir, fmt.Sprintf("delete_%d.txt", i)) - err := os.WriteFile(filePath, []byte("content"), 0o644) + err := os.WriteFile(filePath, []byte("content"), 0o600) require.NoError(t, err) } @@ -536,7 +541,7 @@ func BenchmarkLocalStorage_Delete(b *testing.B) { // Pre-create files for deletion for i := 0; i < b.N; i++ { filePath := filepath.Join(tempDir, fmt.Sprintf("bench_delete_%d.txt", i)) - err := os.WriteFile(filePath, []byte("content"), 0o644) + err := os.WriteFile(filePath, []byte("content"), 0o600) if err != nil { b.Fatal(err) } diff --git a/api/edge/bff/internal/server/ledgerapiimp/create.go b/api/edge/bff/internal/server/ledgerapiimp/create.go index 08bb8fc1..fb452a4d 100644 --- a/api/edge/bff/internal/server/ledgerapiimp/create.go +++ b/api/edge/bff/internal/server/ledgerapiimp/create.go @@ -97,7 +97,9 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token } func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAccount, error) { - defer r.Body.Close() + defer func() { + _ = r.Body.Close() + }() payload := srequest.CreateLedgerAccount{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { diff --git a/api/edge/bff/internal/server/papitemplate/archive.go b/api/edge/bff/internal/server/papitemplate/archive.go index 667c1b9a..d4bb3d0c 100644 --- a/api/edge/bff/internal/server/papitemplate/archive.go +++ b/api/edge/bff/internal/server/papitemplate/archive.go @@ -46,7 +46,7 @@ func (a *ProtectedAPI[T]) archive(r *http.Request, account *model.Account, acces ctx := r.Context() _, err = a.a.DBFactory().TransactionFactory().CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) { - return nil, a.DB.SetArchived(r.Context(), *account.GetID(), organizationRef, objectRef, *archived, *cascade) + return nil, a.DB.SetArchived(ctx, *account.GetID(), organizationRef, objectRef, *archived, *cascade) }) if err != nil { a.Logger.Warn("Failed to change archive property", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r), diff --git a/api/edge/bff/internal/server/paymentapiimp/documents.go b/api/edge/bff/internal/server/paymentapiimp/documents.go index 0aa9c4f6..5244b121 100644 --- a/api/edge/bff/internal/server/paymentapiimp/documents.go +++ b/api/edge/bff/internal/server/paymentapiimp/documents.go @@ -69,7 +69,7 @@ func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Accoun op, err := a.fetchGatewayOperation(r.Context(), gateway.InvokeURI, operationRef) if err != nil { - a.logger.Warn("Failed to fetch gateway operation for document generation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", string(gatewayService)), zap.String("operation_ref", operationRef)) + a.logger.Warn("Failed to fetch gateway operation for document generation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef)) return documentErrorResponse(a.logger, a.Name(), err) } @@ -77,7 +77,7 @@ func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Accoun docResp, err := a.fetchOperationDocument(r.Context(), service.InvokeURI, req) if err != nil { - a.logger.Warn("Failed to fetch operation document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", string(gatewayService)), zap.String("operation_ref", operationRef)) + a.logger.Warn("Failed to fetch operation document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef)) return documentErrorResponse(a.logger, a.Name(), err) } @@ -154,6 +154,7 @@ func operationDocumentResponse(logger mlogger.Logger, source mservice.Type, docR w.Header().Set("Content-Type", mimeType) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) w.WriteHeader(http.StatusOK) + //nolint:gosec // Binary payload is served as attachment with explicit content type. if _, err := w.Write(docResp.GetContent()); err != nil { logger.Warn("Failed to write document response", zap.Error(err)) } @@ -167,15 +168,15 @@ func normalizeGatewayService(raw string) mservice.Type { } switch value { - case string(mservice.ChainGateway): + case mservice.ChainGateway: return mservice.ChainGateway - case string(mservice.TronGateway): + case mservice.TronGateway: return mservice.TronGateway - case string(mservice.MntxGateway): + case mservice.MntxGateway: return mservice.MntxGateway - case string(mservice.PaymentGateway): + case mservice.PaymentGateway: return mservice.PaymentGateway - case string(mservice.TgSettle): + case mservice.TgSettle: return mservice.TgSettle default: return "" @@ -219,7 +220,11 @@ func (a *PaymentAPI) fetchOperationDocument(ctx context.Context, invokeURI strin if err != nil { return nil, merrors.InternalWrap(err, "dial billing documents") } - defer conn.Close() + defer func() { + if closeErr := conn.Close(); closeErr != nil { + a.logger.Warn("Failed to close billing documents connection", zap.Error(closeErr)) + } + }() client := documentsv1.NewDocumentServiceClient(conn) @@ -234,7 +239,11 @@ func (a *PaymentAPI) fetchGatewayOperation(ctx context.Context, invokeURI, opera if err != nil { return nil, merrors.InternalWrap(err, "dial gateway connector") } - defer conn.Close() + defer func() { + if closeErr := conn.Close(); closeErr != nil { + a.logger.Warn("Failed to close gateway connector connection", zap.Error(closeErr)) + } + }() client := connectorv1.NewConnectorServiceClient(conn) @@ -307,7 +316,7 @@ func findGatewayForService(gateways []discovery.GatewaySummary, gatewayService m func operationDocumentRequest(organizationRef string, gatewayService mservice.Type, requestedOperationRef string, op *connectorv1.Operation) *documentsv1.GetOperationDocumentRequest { req := &documentsv1.GetOperationDocumentRequest{ OrganizationRef: strings.TrimSpace(organizationRef), - GatewayService: string(gatewayService), + GatewayService: gatewayService, OperationRef: firstNonEmpty(strings.TrimSpace(op.GetOperationRef()), strings.TrimSpace(requestedOperationRef)), OperationCode: strings.TrimSpace(op.GetType().String()), OperationLabel: operationLabel(op.GetType()), diff --git a/api/edge/bff/internal/server/paymentapiimp/list.go b/api/edge/bff/internal/server/paymentapiimp/list.go index 158b3965..8f930688 100644 --- a/api/edge/bff/internal/server/paymentapiimp/list.go +++ b/api/edge/bff/internal/server/paymentapiimp/list.go @@ -103,6 +103,7 @@ func listPaymentsPage(r *http.Request) (*paginationv1.CursorPageRequest, error) } if cursor == "" && !hasLimit { + //nolint:nilnil // Absent pagination params mean no page request should be sent downstream. return nil, nil } @@ -189,6 +190,7 @@ func firstNonEmpty(values ...string) string { func parseRFC3339Timestamp(raw string, field string) (*timestamppb.Timestamp, error) { trimmed := strings.TrimSpace(raw) if trimmed == "" { + //nolint:nilnil // Empty timestamp filter is represented as (nil, nil). return nil, nil } parsed, err := time.Parse(time.RFC3339, trimmed) diff --git a/api/edge/bff/internal/server/paymentapiimp/pay.go b/api/edge/bff/internal/server/paymentapiimp/pay.go index 88c68896..8a76b69e 100644 --- a/api/edge/bff/internal/server/paymentapiimp/pay.go +++ b/api/edge/bff/internal/server/paymentapiimp/pay.go @@ -102,7 +102,9 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to } func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) { - defer r.Body.Close() + defer func() { + _ = r.Body.Close() + }() payload := &srequest.InitiatePayment{} if err := json.NewDecoder(r.Body).Decode(payload); err != nil { diff --git a/api/edge/bff/internal/server/paymentapiimp/pay_test.go b/api/edge/bff/internal/server/paymentapiimp/pay_test.go index 97d1175a..61198036 100644 --- a/api/edge/bff/internal/server/paymentapiimp/pay_test.go +++ b/api/edge/bff/internal/server/paymentapiimp/pay_test.go @@ -68,7 +68,7 @@ func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) { func invokeInitiateByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder { t.Helper() - req := httptest.NewRequest(http.MethodPost, "/by-quote", bytes.NewBufferString(body)) + req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-quote", bytes.NewBufferString(body)) routeCtx := chi.NewRouteContext() routeCtx.URLParams.Add("organizations_ref", orgRef.Hex()) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx)) diff --git a/api/edge/bff/internal/server/paymentapiimp/paybatch.go b/api/edge/bff/internal/server/paymentapiimp/paybatch.go index 71b81779..9351d7e1 100644 --- a/api/edge/bff/internal/server/paymentapiimp/paybatch.go +++ b/api/edge/bff/internal/server/paymentapiimp/paybatch.go @@ -63,7 +63,9 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc } func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) { - defer r.Body.Close() + defer func() { + _ = r.Body.Close() + }() payload := &srequest.InitiatePayments{} decoder := json.NewDecoder(r.Body) diff --git a/api/edge/bff/internal/server/paymentapiimp/paybatch_test.go b/api/edge/bff/internal/server/paymentapiimp/paybatch_test.go index f59f1201..598a5482 100644 --- a/api/edge/bff/internal/server/paymentapiimp/paybatch_test.go +++ b/api/edge/bff/internal/server/paymentapiimp/paybatch_test.go @@ -10,7 +10,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/tech/sendico/pkg/auth" - "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" @@ -118,7 +117,7 @@ func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefsField(t *testing.T) func newBatchAPI(exec executionClient) *PaymentAPI { return &PaymentAPI{ - logger: mlogger.Logger(zap.NewNop()), + logger: zap.NewNop(), execution: exec, enf: fakeEnforcerForBatch{allowed: true}, oph: mutil.CreatePH(mservice.Organizations), @@ -129,7 +128,7 @@ func newBatchAPI(exec executionClient) *PaymentAPI { func invokeInitiatePaymentsByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder { t.Helper() - req := httptest.NewRequest(http.MethodPost, "/by-multiquote", bytes.NewBufferString(body)) + req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-multiquote", bytes.NewBufferString(body)) routeCtx := chi.NewRouteContext() routeCtx.URLParams.Add("organizations_ref", orgRef.Hex()) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx)) @@ -177,6 +176,7 @@ func (f fakeEnforcerForBatch) Enforce(context.Context, bson.ObjectID, bson.Objec } func (fakeEnforcerForBatch) EnforceBatch(context.Context, []model.PermissionBoundStorable, bson.ObjectID, model.Action) (map[bson.ObjectID]bool, error) { + //nolint:nilnil // Test stub does not provide batch permissions map. return nil, nil } diff --git a/api/edge/bff/internal/server/paymentapiimp/quote.go b/api/edge/bff/internal/server/paymentapiimp/quote.go index 425ccd7a..a9c23e72 100644 --- a/api/edge/bff/internal/server/paymentapiimp/quote.go +++ b/api/edge/bff/internal/server/paymentapiimp/quote.go @@ -126,7 +126,9 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke } func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) { - defer r.Body.Close() + defer func() { + _ = r.Body.Close() + }() payload := &srequest.QuotePayment{} if err := json.NewDecoder(r.Body).Decode(payload); err != nil { @@ -140,7 +142,9 @@ func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) { } func decodeQuotePaymentsPayload(r *http.Request) (*srequest.QuotePayments, error) { - defer r.Body.Close() + defer func() { + _ = r.Body.Close() + }() payload := &srequest.QuotePayments{} if err := json.NewDecoder(r.Body).Decode(payload); err != nil { diff --git a/api/edge/bff/internal/server/paymentapiimp/service.go b/api/edge/bff/internal/server/paymentapiimp/service.go index 3424a98d..e0a99f11 100644 --- a/api/edge/bff/internal/server/paymentapiimp/service.go +++ b/api/edge/bff/internal/server/paymentapiimp/service.go @@ -256,6 +256,7 @@ func (c *grpcQuotationClient) callContext(ctx context.Context) (context.Context, if timeout <= 0 { timeout = 3 * time.Second } + //nolint:gosec // Caller receives cancel func and defers it in every call path. return context.WithTimeout(ctx, timeout) } @@ -271,7 +272,7 @@ func (a *PaymentAPI) initDiscoveryClient(cfg *eapi.Config) error { if err != nil { return err } - client, err := discovery.NewClient(a.logger, broker, nil, string(a.Name())) + client, err := discovery.NewClient(a.logger, broker, nil, a.Name()) if err != nil { return err } diff --git a/api/edge/bff/internal/server/permissionsimp/changepolicies.go b/api/edge/bff/internal/server/permissionsimp/changepolicies.go index b240a1da..b9030a3e 100644 --- a/api/edge/bff/internal/server/permissionsimp/changepolicies.go +++ b/api/edge/bff/internal/server/permissionsimp/changepolicies.go @@ -90,5 +90,5 @@ func (a *PermissionsAPI) changePoliciesImp( } } - return nil, nil + return struct{}{}, nil } diff --git a/api/edge/bff/internal/server/walletapiimp/balance.go b/api/edge/bff/internal/server/walletapiimp/balance.go index 5e081783..6b0f7d17 100644 --- a/api/edge/bff/internal/server/walletapiimp/balance.go +++ b/api/edge/bff/internal/server/walletapiimp/balance.go @@ -223,6 +223,7 @@ func (a *WalletAPI) queryBalanceFromGateways(ctx context.Context, gateways []dis a.logger.Debug("Wallet balance fan-out completed without result", zap.String("organization_ref", organizationRef), zap.String("wallet_ref", walletRef)) + //nolint:nilnil // No gateway returned a balance and no hard error occurred. return nil, nil } @@ -238,7 +239,11 @@ func (a *WalletAPI) queryGatewayBalance(ctx context.Context, gateway discovery.G if err != nil { return nil, merrors.InternalWrap(err, "dial gateway") } - defer conn.Close() + defer func() { + if closeErr := conn.Close(); closeErr != nil { + a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID)) + } + }() client := connectorv1.NewConnectorServiceClient(conn) diff --git a/api/edge/bff/internal/server/walletapiimp/create.go b/api/edge/bff/internal/server/walletapiimp/create.go index 56eab75e..2e982189 100644 --- a/api/edge/bff/internal/server/walletapiimp/create.go +++ b/api/edge/bff/internal/server/walletapiimp/create.go @@ -173,7 +173,11 @@ func (a *WalletAPI) createWalletOnGateway(ctx context.Context, gateway discovery if err != nil { return "", merrors.InternalWrap(err, "dial gateway") } - defer conn.Close() + defer func() { + if closeErr := conn.Close(); closeErr != nil { + a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID)) + } + }() client := connectorv1.NewConnectorServiceClient(conn) diff --git a/api/edge/bff/internal/server/walletapiimp/list.go b/api/edge/bff/internal/server/walletapiimp/list.go index 38c4d8c8..ac1a095a 100644 --- a/api/edge/bff/internal/server/walletapiimp/list.go +++ b/api/edge/bff/internal/server/walletapiimp/list.go @@ -226,7 +226,11 @@ func (a *WalletAPI) queryGateway(ctx context.Context, gateway discovery.GatewayS if err != nil { return nil, merrors.InternalWrap(err, "dial gateway") } - defer conn.Close() + defer func() { + if closeErr := conn.Close(); closeErr != nil { + a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID)) + } + }() client := connectorv1.NewConnectorServiceClient(conn) diff --git a/api/edge/bff/internal/server/walletapiimp/routing.go b/api/edge/bff/internal/server/walletapiimp/routing.go index f9072f74..09fbc470 100644 --- a/api/edge/bff/internal/server/walletapiimp/routing.go +++ b/api/edge/bff/internal/server/walletapiimp/routing.go @@ -64,18 +64,21 @@ func (a *WalletAPI) rememberWalletRoute(ctx context.Context, organizationRef str func (a *WalletAPI) walletRoute(ctx context.Context, organizationRef string, walletRef string) (*model.ChainWalletRoute, error) { if a.routes == nil { + //nolint:nilnil // Routing cache is optional and may be disabled. return nil, nil } walletRef = strings.TrimSpace(walletRef) organizationRef = strings.TrimSpace(organizationRef) if walletRef == "" || organizationRef == "" { + //nolint:nilnil // Missing route keys mean no cached route. return nil, nil } route, err := a.routes.Get(ctx, organizationRef, walletRef) if err != nil { if errors.Is(err, merrors.ErrNoData) { + //nolint:nilnil // Route not found in cache. return nil, nil } return nil, err diff --git a/api/edge/bff/internal/server/walletapiimp/service.go b/api/edge/bff/internal/server/walletapiimp/service.go index 4007acdd..2ee23559 100644 --- a/api/edge/bff/internal/server/walletapiimp/service.go +++ b/api/edge/bff/internal/server/walletapiimp/service.go @@ -129,7 +129,7 @@ func (a *WalletAPI) initDiscoveryClient(cfg *eapi.Config) error { if err != nil { return err } - client, err := discovery.NewClient(a.logger, broker, nil, string(a.Name())) + client, err := discovery.NewClient(a.logger, broker, nil, a.Name()) if err != nil { return err } diff --git a/api/edge/bff/main.go b/api/edge/bff/main.go index 35740028..941a50f2 100644 --- a/api/edge/bff/main.go +++ b/api/edge/bff/main.go @@ -9,8 +9,8 @@ import ( ) // generate translations -// go:generate Users/stephandeshevikh/go/bin/go18n extract -// go:generate Users/stephandeshevikh/go/bin/go18n merge +//go:generate Users/stephandeshevikh/go/bin/go18n extract +//go:generate Users/stephandeshevikh/go/bin/go18n merge // lint go code // docker run -t --rm -v $(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v --timeout 10m0s --enable-all -D ireturn -D wrapcheck -D varnamelen -D tagliatelle -D nosnakecase -D gochecknoglobals -D nlreturn -D stylecheck -D lll -D wsl -D scopelint -D varcheck -D exhaustivestruct -D golint -D maligned -D interfacer -D ifshort -D structcheck -D deadcode -D godot -D depguard -D tagalign diff --git a/api/edge/callbacks/.golangci.yml b/api/edge/callbacks/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/edge/callbacks/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/edge/callbacks/internal/config/service.go b/api/edge/callbacks/internal/config/service.go index 88aa65fb..d3aca7ce 100644 --- a/api/edge/callbacks/internal/config/service.go +++ b/api/edge/callbacks/internal/config/service.go @@ -28,6 +28,7 @@ func (s *service) Load(path string) (*Config, error) { return nil, merrors.InvalidArgument("config path is required", "path") } + //nolint:gosec // Configuration file path is provided by service startup configuration. data, err := os.ReadFile(path) if err != nil { s.logger.Error("Failed to read config file", zap.String("path", path), zap.Error(err)) diff --git a/api/edge/callbacks/internal/delivery/service.go b/api/edge/callbacks/internal/delivery/service.go index 60857870..011b56a0 100644 --- a/api/edge/callbacks/internal/delivery/service.go +++ b/api/edge/callbacks/internal/delivery/service.go @@ -108,7 +108,7 @@ func (s *service) Start(ctx context.Context) { if runCtx == nil { runCtx = context.Background() } - runCtx, s.cancel = context.WithCancel(runCtx) + runCtx, s.cancel = context.WithCancel(runCtx) //nolint:gosec // canceled by Stop; service lifecycle outlives Start scope for i := 0; i < s.cfg.WorkerConcurrency; i++ { workerID := "worker-" + strconv.Itoa(i+1) @@ -143,6 +143,10 @@ func (s *service) runWorker(ctx context.Context, workerID string) { now := time.Now().UTC() task, err := s.tasks.LockNextTask(ctx, now, workerID, s.cfg.LockTTL) if err != nil { + if errors.Is(err, merrors.ErrNoData) { + time.Sleep(s.cfg.WorkerPoll) + continue + } s.logger.Warn("Failed to lock next task", zap.String("worker_id", workerID), zap.Error(err)) time.Sleep(s.cfg.WorkerPoll) continue diff --git a/api/edge/callbacks/internal/ingest/service.go b/api/edge/callbacks/internal/ingest/service.go index 875d5915..51f09f75 100644 --- a/api/edge/callbacks/internal/ingest/service.go +++ b/api/edge/callbacks/internal/ingest/service.go @@ -106,7 +106,7 @@ func (s *service) Start(ctx context.Context) { if runCtx == nil { runCtx = context.Background() } - runCtx, s.cancel = context.WithCancel(runCtx) + runCtx, s.cancel = context.WithCancel(runCtx) //nolint:gosec // canceled by Stop; service lifecycle outlives Start scope s.wg.Add(1) go func() { diff --git a/api/edge/callbacks/internal/retry/service.go b/api/edge/callbacks/internal/retry/service.go index 5c60c8ba..aa9b84df 100644 --- a/api/edge/callbacks/internal/retry/service.go +++ b/api/edge/callbacks/internal/retry/service.go @@ -14,6 +14,7 @@ type service struct { // New creates retry policy service. func New() Policy { + //nolint:gosec // Backoff jitter is non-cryptographic and only needs pseudo-random distribution. return &service{rnd: rand.New(rand.NewSource(time.Now().UnixNano()))} } diff --git a/api/edge/callbacks/internal/server/internal/serverimp.go b/api/edge/callbacks/internal/server/internal/serverimp.go index 83e00ea7..5084cae4 100644 --- a/api/edge/callbacks/internal/server/internal/serverimp.go +++ b/api/edge/callbacks/internal/server/internal/serverimp.go @@ -154,6 +154,7 @@ func (i *Imp) Start() error { runCtx, cancel := context.WithCancel(context.Background()) i.runCancel = cancel + defer cancel() i.ingest.Start(runCtx) i.delivery.Start(runCtx) i.opServer.SetStatus(health.SSRunning) diff --git a/api/edge/callbacks/internal/storage/service.go b/api/edge/callbacks/internal/storage/service.go index 6b832861..3a023acd 100644 --- a/api/edge/callbacks/internal/storage/service.go +++ b/api/edge/callbacks/internal/storage/service.go @@ -379,7 +379,7 @@ func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID st candidates, err := mutil.GetObjects[taskDoc](ctx, r.logger, query, nil, r.repo) if err != nil { if errors.Is(err, merrors.ErrNoData) { - return nil, nil + return nil, merrors.ErrNoData } return nil, merrors.InternalWrap(err, "callbacks task query failed") } @@ -418,7 +418,7 @@ func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID st return mapTaskDoc(locked), nil } - return nil, nil + return nil, merrors.ErrNoData } func (r *taskStore) MarkDelivered(ctx context.Context, taskID bson.ObjectID, httpCode int, latency time.Duration, at time.Time) error { diff --git a/api/fx/ingestor/.golangci.yml b/api/fx/ingestor/.golangci.yml index 88d33b5c..60a80daf 100644 --- a/api/fx/ingestor/.golangci.yml +++ b/api/fx/ingestor/.golangci.yml @@ -1,196 +1,47 @@ -# See the dedicated "version" documentation section. version: "2" linters: - # Default set of linters. - # The value can be: - # - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default - # - `all`: enables all linters by default. - # - `none`: disables all linters by default. - # - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`). - # Default: standard - default: all - # Enable specific linter. + default: none enable: - - arangolint - - asasalint - - asciicheck - - bidichk - bodyclose - canonicalheader - - containedctx - - contextcheck - copyloopvar - - cyclop - - decorder - - dogsled - - dupl - - dupword - durationcheck - - embeddedstructfieldcheck - - err113 - errcheck - errchkjson - errname - errorlint - - exhaustive - - exptostd - - fatcontext - - forbidigo - - forcetypeassert - - funcorder - - funlen - - ginkgolinter - - gocheckcompilerdirectives - - gochecknoglobals - - gochecknoinits - - gochecksumtype - - gocognit - - goconst - - gocritic - - gocyclo - - godoclint - - godot - - godox - - goheader - - gomodguard - - goprintffuncname - gosec - - gosmopolitan - govet - - grouper - - iface - - importas - - inamedparam - ineffassign - - interfacebloat - - intrange - - iotamixing - - ireturn - - lll - - loggercheck - - maintidx - - makezero - - mirror - - misspell - - mnd - - modernize - - musttag - - nakedret - - nestif - nilerr - nilnesserr - nilnil - - nlreturn - noctx - - noinlineerr - - nolintlint - - nonamedreturns - - nosprintfhostport - - paralleltest - - perfsprint - - prealloc - - predeclared - - promlinter - - protogetter - - reassign - - recvcheck - - revive - rowserrcheck - - sloglint - - spancheck - sqlclosecheck - staticcheck - - tagalign - - tagliatelle - - testableexamples - - testifylint - - testpackage - - thelper - - tparallel - unconvert - - unparam - - unqueryvet - - unused - - usestdlibvars - - usetesting - - varnamelen - wastedassign - - whitespace - - wsl_v5 - - zerologlint - # Disable specific linters. - disable: + disable: - depguard - exhaustruct - gochecknoglobals + - gochecknoinits - gomoddirectives - wrapcheck - - wsl - # All available settings of specific linters. - # See the dedicated "linters.settings" documentation section. - settings: - wsl_v5: - allow-first-in-block: true - allow-whole-block: false - branch-max-lines: 2 - - # Defines a set of rules to ignore issues. - # It does not skip the analysis, and so does not ignore "typecheck" errors. - exclusions: - # Mode of the generated files analysis. - # - # - `strict`: sources are excluded by strictly following the Go generated file convention. - # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` - # This line must appear before the first non-comment, non-blank text in the file. - # https://go.dev/s/generatedcode - # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. - # - `disable`: disable the generated files exclusion. - # - # Default: strict - generated: lax - # Log a warning if an exclusion rule is unused. - # Default: false - warn-unused: true - # Predefined exclusion rules. - # Default: [] - presets: - - comments - - std-error-handling - - common-false-positives - - legacy - # Excluding configuration per-path, per-linter, per-text and per-source. - rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - funlen - - gocyclo - - errcheck - - dupl - - gosec - # Run some linter only for test files by excluding its issues for everything else. - - path-except: _test\.go - linters: - - forbidigo - # Exclude known linters from partially hard-vendored code, - # which is impossible to exclude via `nolint` comments. - # `/` will be replaced by the current OS file path separator to properly work on Windows. - - path: internal/hmac/ - text: "weak cryptographic primitive" - linters: - - gosec - # Exclude some `staticcheck` messages. - - linters: - - staticcheck - text: "SA9003:" - # Exclude `lll` issues for long lines with `go:generate`. - - linters: - - lll - source: "^//go:generate " - # Which file paths to exclude: they will be analyzed, but issues from them won't be reported. - # "/" will be replaced by the current OS file path separator to properly work on Windows. - # Default: [] - paths: [] - # Which file paths to not exclude. - # Default: [] - paths-except: [] + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/fx/ingestor/internal/config/config.go b/api/fx/ingestor/internal/config/config.go index f2e6039b..ed196c23 100644 --- a/api/fx/ingestor/internal/config/config.go +++ b/api/fx/ingestor/internal/config/config.go @@ -31,6 +31,7 @@ func Load(path string) (*Config, error) { return nil, merrors.InvalidArgument("config: path is empty") } + //nolint:gosec // config path is provided by process startup arguments/config. data, err := os.ReadFile(path) if err != nil { return nil, merrors.InternalWrap(err, "config: failed to read file") @@ -73,8 +74,10 @@ func Load(path string) (*Config, error) { } if _, ok := sourceSet[driver]; !ok { - return nil, merrors.InvalidArgument( //nolint:lll - "config: pair references unknown source: "+driver.String(), "pairs."+driver.String()) + return nil, merrors.InvalidArgument( + "config: pair references unknown source: "+driver.String(), + "pairs."+driver.String(), + ) } processed := make([]PairConfig, len(pairList)) @@ -86,14 +89,16 @@ func Load(path string) (*Config, error) { pair.Symbol = strings.TrimSpace(pair.Symbol) if pair.Base == "" || pair.Quote == "" || pair.Symbol == "" { - return nil, merrors.InvalidArgument( //nolint:lll - "config: pair entries must define base, quote, and symbol", "pairs."+driver.String()) + return nil, merrors.InvalidArgument( + "config: pair entries must define base, quote, and symbol", + "pairs."+driver.String(), + ) } if strings.TrimSpace(pair.Provider) == "" { pair.Provider = strings.ToLower(driver.String()) } - + processed[idx] = pair flattened = append(flattened, Pair{ PairConfig: pair, diff --git a/api/fx/ingestor/internal/ingestor/service_test.go b/api/fx/ingestor/internal/ingestor/service_test.go index 16f9b2e1..49e6bb7a 100644 --- a/api/fx/ingestor/internal/ingestor/service_test.go +++ b/api/fx/ingestor/internal/ingestor/service_test.go @@ -14,6 +14,8 @@ import ( "go.uber.org/zap" ) +var errNoSnapshot = errors.New("snapshot not found") + func TestParseDecimal(t *testing.T) { got, err := parseDecimal("123.456") if err != nil { @@ -191,19 +193,9 @@ func (r *ratesStoreStub) UpsertSnapshot(_ context.Context, snapshot *model.RateS } func (r *ratesStoreStub) LatestSnapshot(context.Context, model.CurrencyPair, string) (*model.RateSnapshot, error) { - return nil, nil + return nil, errNoSnapshot } -type repositoryStub struct { - rates storage.RatesStore -} - -func (r *repositoryStub) Ping(context.Context) error { return nil } -func (r *repositoryStub) Rates() storage.RatesStore { return r.rates } -func (r *repositoryStub) Quotes() storage.QuotesStore { return nil } -func (r *repositoryStub) Pairs() storage.PairStore { return nil } -func (r *repositoryStub) Currencies() storage.CurrencyStore { return nil } - type connectorStub struct { id mmarket.Driver ticker *mmarket.Ticker diff --git a/api/fx/ingestor/internal/market/binance/connector.go b/api/fx/ingestor/internal/market/binance/connector.go index 26ec146e..374a19a5 100644 --- a/api/fx/ingestor/internal/market/binance/connector.go +++ b/api/fx/ingestor/internal/market/binance/connector.go @@ -116,7 +116,11 @@ func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmo return nil, merrors.InternalWrap(err, "binance: request failed") } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + c.logger.Warn("Failed to close Binance response body", zap.Error(closeErr)) + } + }() if resp.StatusCode != http.StatusOK { c.logger.Warn("Binance returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode)) diff --git a/api/fx/ingestor/internal/market/cbr/connector.go b/api/fx/ingestor/internal/market/cbr/connector.go index f3d90c70..c2fad0d8 100644 --- a/api/fx/ingestor/internal/market/cbr/connector.go +++ b/api/fx/ingestor/internal/market/cbr/connector.go @@ -122,9 +122,11 @@ func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Conne logger, client, httpClientOptions{ - userAgent: userAgent, - accept: acceptHeader, - referer: referer, + userAgent: userAgent, + accept: acceptHeader, + referer: referer, + allowedScheme: parsed.Scheme, + allowedHost: parsed.Host, }, ), base: strings.TrimRight(parsed.String(), "/"), @@ -200,7 +202,11 @@ func (c *cbrConnector) refreshDirectory() error { ) return merrors.InternalWrap(err, "cbr: directory request failed") } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + c.logger.Warn("Failed to close CBR daily response body", zap.Error(closeErr)) + } + }() if resp.StatusCode != http.StatusOK { c.logger.Warn( @@ -258,7 +264,11 @@ func (c *cbrConnector) fetchDailyRate(ctx context.Context, valute valuteInfo) (s ) return "", merrors.InternalWrap(err, "cbr: daily request failed") } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + c.logger.Warn("Failed to close CBR historical response body", zap.Error(closeErr)) + } + }() if resp.StatusCode != http.StatusOK { c.logger.Warn( @@ -326,7 +336,11 @@ func (c *cbrConnector) fetchHistoricalRate( //nolint:funlen ) return "", merrors.InternalWrap(err, "cbr: historical request failed") } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + c.logger.Warn("Failed to close CBR historical response body", zap.Error(closeErr)) + } + }() if resp.StatusCode != http.StatusOK { c.logger.Warn( diff --git a/api/fx/ingestor/internal/market/cbr/http_client.go b/api/fx/ingestor/internal/market/cbr/http_client.go index c6878b7e..a3d78c79 100644 --- a/api/fx/ingestor/internal/market/cbr/http_client.go +++ b/api/fx/ingestor/internal/market/cbr/http_client.go @@ -2,7 +2,10 @@ package cbr import ( "context" + "errors" + "fmt" "net/http" + "net/url" "strings" "github.com/tech/sendico/pkg/mlogger" @@ -14,17 +17,28 @@ const ( defaultAccept = "application/xml,text/xml;q=0.9,*/*;q=0.8" ) +var ( + errNilRequestURL = errors.New("http_client: request URL is nil") + errRelativeRequestURL = errors.New("http_client: request URL must be absolute") + errUnexpectedURLScheme = errors.New("http_client: unexpected URL scheme") + errUnexpectedURLHost = errors.New("http_client: unexpected URL host") +) + // httpClient wraps http.Client to ensure CBR requests always carry required headers. type httpClient struct { - client *http.Client - headers http.Header - logger mlogger.Logger + client *http.Client + headers http.Header + logger mlogger.Logger + allowedScheme string + allowedHost string } type httpClientOptions struct { - userAgent string - accept string - referer string + userAgent string + accept string + referer string + allowedScheme string + allowedHost string } func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOptions) *httpClient { @@ -42,6 +56,20 @@ func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOp if strings.TrimSpace(referer) == "" { referer = defaultCBRBaseURL } + + allowedScheme := strings.ToLower(strings.TrimSpace(opts.allowedScheme)) + allowedHost := strings.ToLower(strings.TrimSpace(opts.allowedHost)) + + if allowedScheme == "" || allowedHost == "" { + if parsed, err := url.Parse(referer); err == nil { + if allowedScheme == "" { + allowedScheme = strings.ToLower(parsed.Scheme) + } + if allowedHost == "" { + allowedHost = strings.ToLower(parsed.Host) + } + } + } httpLogger := logger.Named("http_client") headers := make(http.Header, 3) @@ -53,9 +81,11 @@ func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOp zap.String("accept", accept), zap.String("referrer", referer)) return &httpClient{ - client: client, - headers: headers, - logger: httpLogger, + client: client, + headers: headers, + logger: httpLogger, + allowedScheme: allowedScheme, + allowedHost: allowedHost, } } @@ -74,6 +104,13 @@ func (h *httpClient) Do(req *http.Request) (*http.Response, error) { enriched.Header.Add(key, value) } } + + if err := h.validateRequestTarget(enriched.URL); err != nil { + h.logger.Warn("HTTP request blocked by target validation", zap.Error(err), zap.String("method", req.Method)) + return nil, err + } + + //nolint:gosec // request URL is constrained in validateRequestTarget before any outbound call. r, err := h.client.Do(enriched) if err != nil { h.logger.Warn("HTTP request failed", zap.Error(err), zap.String("method", req.Method), @@ -85,3 +122,26 @@ func (h *httpClient) Do(req *http.Request) (*http.Response, error) { func (h *httpClient) headerValue(name string) string { return h.headers.Get(name) } + +func (h *httpClient) validateRequestTarget(requestURL *url.URL) error { + if requestURL == nil { + return errNilRequestURL + } + + if !requestURL.IsAbs() { + return errRelativeRequestURL + } + + scheme := strings.ToLower(requestURL.Scheme) + host := strings.ToLower(requestURL.Host) + + if h.allowedScheme != "" && scheme != h.allowedScheme { + return fmt.Errorf("%w: %q", errUnexpectedURLScheme, requestURL.Scheme) + } + + if h.allowedHost != "" && host != h.allowedHost { + return fmt.Errorf("%w: %q", errUnexpectedURLHost, requestURL.Host) + } + + return nil +} diff --git a/api/fx/ingestor/internal/market/coingecko/connector.go b/api/fx/ingestor/internal/market/coingecko/connector.go index 52234a5b..d8d2ecd8 100644 --- a/api/fx/ingestor/internal/market/coingecko/connector.go +++ b/api/fx/ingestor/internal/market/coingecko/connector.go @@ -125,7 +125,11 @@ func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*m return nil, merrors.InternalWrap(err, "coingecko: request failed") } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + c.logger.Warn("Failed to close CoinGecko response body", zap.Error(closeErr)) + } + }() if resp.StatusCode != http.StatusOK { c.logger.Warn("CoinGecko returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode)) diff --git a/api/fx/ingestor/internal/market/common/settings.go b/api/fx/ingestor/internal/market/common/settings.go index 26951a62..454c962a 100644 --- a/api/fx/ingestor/internal/market/common/settings.go +++ b/api/fx/ingestor/internal/market/common/settings.go @@ -1,4 +1,5 @@ -package common //nolint:revive // package provides shared market connector utilities +// Package common provides shared market connector utilities. +package common import ( "strconv" diff --git a/api/fx/ingestor/main.go b/api/fx/ingestor/main.go index 468ae6d1..1d239cae 100644 --- a/api/fx/ingestor/main.go +++ b/api/fx/ingestor/main.go @@ -32,7 +32,9 @@ func main() { appVersion := appversion.Create() if *versionFlag { - fmt.Fprintln(os.Stdout, appVersion.Print()) + if _, err := fmt.Fprintln(os.Stdout, appVersion.Print()); err != nil { + return + } return } diff --git a/api/fx/oracle/.golangci.yml b/api/fx/oracle/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/fx/oracle/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/fx/oracle/client/client.go b/api/fx/oracle/client/client.go index 3f7dbe66..22990821 100644 --- a/api/fx/oracle/client/client.go +++ b/api/fx/oracle/client/client.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "math" "strings" "time" @@ -134,7 +135,11 @@ func (c *oracleClient) LatestRate(ctx context.Context, req LatestRateParams) (*R return nil, merrors.InvalidArgument("oracle: pair is required") } - callCtx, cancel := c.callContext(ctx) + callCtx := ctx + cancel := func() {} + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + callCtx, cancel = context.WithTimeout(ctx, c.cfg.CallTimeout) + } defer cancel() resp, err := c.client.LatestRate(callCtx, &oraclev1.LatestRateRequest{ @@ -165,7 +170,11 @@ func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote return nil, merrors.InvalidArgument("oracle: exactly one of base_amount or quote_amount must be set") } - callCtx, cancel := c.callContext(ctx) + callCtx := ctx + cancel := func() {} + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + callCtx, cancel = context.WithTimeout(ctx, c.cfg.CallTimeout) + } defer cancel() protoReq := &oraclev1.GetQuoteRequest{ @@ -179,7 +188,11 @@ func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote protoReq.TtlMs = req.TTL.Milliseconds() } if req.MaxAge > 0 { - protoReq.MaxAgeMs = int32(req.MaxAge.Milliseconds()) + maxAgeMs := req.MaxAge.Milliseconds() + if maxAgeMs > math.MaxInt32 { + maxAgeMs = math.MaxInt32 + } + protoReq.MaxAgeMs = int32(maxAgeMs) } if baseSupplied { protoReq.AmountInput = &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: req.BaseAmount} @@ -197,13 +210,6 @@ func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote return fromProtoQuote(resp.GetQuote()), nil } -func (c *oracleClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { - if _, ok := ctx.Deadline(); ok { - return context.WithCancel(ctx) - } - return context.WithTimeout(ctx, c.cfg.CallTimeout) -} - func toProtoMeta(meta RequestMeta) *oraclev1.RequestMeta { if meta.TenantRef == "" && meta.OrganizationRef == "" && meta.Trace == nil { return nil diff --git a/api/fx/oracle/internal/service/oracle/cross.go b/api/fx/oracle/internal/service/oracle/cross.go index 10e5d54c..863b15ad 100644 --- a/api/fx/oracle/internal/service/oracle/cross.go +++ b/api/fx/oracle/internal/service/oracle/cross.go @@ -2,6 +2,7 @@ package oracle import ( "context" + "errors" "fmt" "math/big" "strings" @@ -99,17 +100,28 @@ func buildPriceSet(rate *model.RateSnapshot) (priceSet, error) { return priceSet{}, merrors.InvalidArgument("oracle: cross rate requires underlying snapshot") } ask, err := parsePrice(rate.Ask) - if err != nil { + if err != nil && !errors.Is(err, merrors.ErrNoData) { return priceSet{}, err } + if errors.Is(err, merrors.ErrNoData) { + ask = nil + } + bid, err := parsePrice(rate.Bid) - if err != nil { + if err != nil && !errors.Is(err, merrors.ErrNoData) { return priceSet{}, err } + if errors.Is(err, merrors.ErrNoData) { + bid = nil + } + mid, err := parsePrice(rate.Mid) - if err != nil { + if err != nil && !errors.Is(err, merrors.ErrNoData) { return priceSet{}, err } + if errors.Is(err, merrors.ErrNoData) { + mid = nil + } if ask == nil && bid == nil { if mid == nil { @@ -141,7 +153,7 @@ func buildPriceSet(rate *model.RateSnapshot) (priceSet, error) { func parsePrice(value string) (*big.Rat, error) { if strings.TrimSpace(value) == "" { - return nil, nil + return nil, merrors.ErrNoData } return ratFromString(value) } diff --git a/api/fx/oracle/internal/service/oracle/service.go b/api/fx/oracle/internal/service/oracle/service.go index 66198641..7d9f2726 100644 --- a/api/fx/oracle/internal/service/oracle/service.go +++ b/api/fx/oracle/internal/service/oracle/service.go @@ -110,7 +110,7 @@ func (s *Service) startDiscoveryAnnouncer() { InvokeURI: s.invokeURI, Version: appversion.Create().Short(), } - s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.FXOracle), announce) + s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.FXOracle, announce) s.announcer.Start() } diff --git a/api/fx/oracle/internal/service/oracle/service_test.go b/api/fx/oracle/internal/service/oracle/service_test.go index edddd0c4..f7e962c6 100644 --- a/api/fx/oracle/internal/service/oracle/service_test.go +++ b/api/fx/oracle/internal/service/oracle/service_test.go @@ -72,7 +72,7 @@ func (q *quotesStoreStub) Consume(ctx context.Context, ref, ledger string, when if q.consumeFn != nil { return q.consumeFn(ctx, ref, ledger, when) } - return nil, nil + return nil, merrors.ErrNoData } func (q *quotesStoreStub) ExpireIssuedBefore(ctx context.Context, cutoff time.Time) (int, error) { diff --git a/api/fx/storage/.golangci.yml b/api/fx/storage/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/fx/storage/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/fx/storage/model/quote.go b/api/fx/storage/model/quote.go index 1274e132..41e83166 100644 --- a/api/fx/storage/model/quote.go +++ b/api/fx/storage/model/quote.go @@ -47,13 +47,13 @@ func (q *Quote) MarkConsumed(ledgerTxnRef string, consumedAt time.Time) { q.ConsumedByLedgerTxnRef = ledgerTxnRef ts := consumedAt.UnixMilli() q.ConsumedAtUnixMs = &ts - q.Base.Update() + q.Update() } // MarkExpired marks the quote as expired. func (q *Quote) MarkExpired() { q.Status = QuoteStatusExpired - q.Base.Update() + q.Update() } // IsExpired reports whether the quote has passed its expiration instant. diff --git a/api/fx/storage/mongo/store/currency_test.go b/api/fx/storage/mongo/store/currency_test.go index 53c8d719..bc880648 100644 --- a/api/fx/storage/mongo/store/currency_test.go +++ b/api/fx/storage/mongo/store/currency_test.go @@ -35,8 +35,8 @@ func TestCurrencyStoreGet(t *testing.T) { func TestCurrencyStoreList(t *testing.T) { repo := &repoStub{ - findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error { - return runDecoderWithDocs(t, decode, &model.Currency{Code: "USD"}) + findManyFn: func(ctx context.Context, _ builder.Query, decode rd.DecodingFunc) error { + return runDecoderWithDocs(ctx, t, decode, &model.Currency{Code: "USD"}) }, } store := ¤cyStore{logger: zap.NewNop(), repo: repo} diff --git a/api/fx/storage/mongo/store/pair_test.go b/api/fx/storage/mongo/store/pair_test.go index 211608a8..6f02c129 100644 --- a/api/fx/storage/mongo/store/pair_test.go +++ b/api/fx/storage/mongo/store/pair_test.go @@ -16,11 +16,11 @@ import ( func TestPairStoreListEnabled(t *testing.T) { repo := &repoStub{ - findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error { + findManyFn: func(ctx context.Context, _ builder.Query, decode rd.DecodingFunc) error { docs := []interface{}{ &model.Pair{Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}}, } - return runDecoderWithDocs(t, decode, docs...) + return runDecoderWithDocs(ctx, t, decode, docs...) }, } store := &pairStore{logger: zap.NewNop(), repo: repo} diff --git a/api/fx/storage/mongo/store/rates_test.go b/api/fx/storage/mongo/store/rates_test.go index 66f64dfd..281d9a4d 100644 --- a/api/fx/storage/mongo/store/rates_test.go +++ b/api/fx/storage/mongo/store/rates_test.go @@ -70,9 +70,9 @@ func TestRatesStoreUpsertUpdate(t *testing.T) { func TestRatesStoreLatestSnapshot(t *testing.T) { now := time.Now().UnixMilli() repo := &repoStub{ - findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error { + findManyFn: func(ctx context.Context, _ builder.Query, decode rd.DecodingFunc) error { doc := &model.RateSnapshot{RateRef: "latest", AsOfUnixMs: now} - return runDecoderWithDocs(t, decode, doc) + return runDecoderWithDocs(ctx, t, decode, doc) }, } diff --git a/api/fx/storage/mongo/store/testing_helpers_test.go b/api/fx/storage/mongo/store/testing_helpers_test.go index 6d10d7f7..22602ced 100644 --- a/api/fx/storage/mongo/store/testing_helpers_test.go +++ b/api/fx/storage/mongo/store/testing_helpers_test.go @@ -176,16 +176,18 @@ func cloneCurrency(t *testing.T, obj storable.Storable) *model.Currency { return © } -func runDecoderWithDocs(t *testing.T, decode rd.DecodingFunc, docs ...interface{}) error { +func runDecoderWithDocs(ctx context.Context, t *testing.T, decode rd.DecodingFunc, docs ...interface{}) error { t.Helper() cur, err := mongo.NewCursorFromDocuments(docs, nil, nil) if err != nil { t.Fatalf("failed to create cursor: %v", err) } - defer cur.Close(context.Background()) + defer func() { + _ = cur.Close(ctx) + }() if len(docs) > 0 { - if !cur.Next(context.Background()) { + if !cur.Next(ctx) { return cur.Err() } } diff --git a/api/gateway/aurora/.golangci.yml b/api/gateway/aurora/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/gateway/aurora/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/gateway/aurora/client/client.go b/api/gateway/aurora/client/client.go index e430bde5..75f91c90 100644 --- a/api/gateway/aurora/client/client.go +++ b/api/gateway/aurora/client/client.go @@ -87,7 +87,7 @@ func (g *gatewayClient) callContext(ctx context.Context, method string) (context } g.logger.Info("Aurora gateway client call timeout applied", fields...) } - return context.WithTimeout(ctx, timeout) + return context.WithTimeout(ctx, timeout) //nolint:gosec // cancel func is always invoked by call sites } func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) { diff --git a/api/gateway/aurora/internal/server/internal/serverimp.go b/api/gateway/aurora/internal/server/internal/serverimp.go index 2e21e784..417bc6f4 100644 --- a/api/gateway/aurora/internal/server/internal/serverimp.go +++ b/api/gateway/aurora/internal/server/internal/serverimp.go @@ -434,7 +434,7 @@ func buildGatewayLimits(cfg limitsConfig) *gatewayv1.Limits { if bucket == "" { continue } - limits.VelocityLimit[bucket] = int32(value) + limits.VelocityLimit[bucket] = int32(value) //nolint:gosec // velocity limits are validated config values } } @@ -450,7 +450,7 @@ func buildGatewayLimits(cfg limitsConfig) *gatewayv1.Limits { MinAmount: strings.TrimSpace(override.MinAmount), MaxAmount: strings.TrimSpace(override.MaxAmount), MaxFee: strings.TrimSpace(override.MaxFee), - MaxOps: int32(override.MaxOps), + MaxOps: int32(override.MaxOps), //nolint:gosec // max ops is a validated config value } } } @@ -546,11 +546,12 @@ func (i *Imp) startHTTPCallbackServer(svc *auroraservice.Service, cfg callbackRu }) server := &http.Server{ - Addr: cfg.Address, - Handler: router, + Addr: cfg.Address, + Handler: router, + ReadHeaderTimeout: 5 * time.Second, } - ln, err := net.Listen("tcp", cfg.Address) + ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", cfg.Address) if err != nil { return err } diff --git a/api/gateway/aurora/internal/service/gateway/card_payout_store_test.go b/api/gateway/aurora/internal/service/gateway/card_payout_store_test.go index b6d0367e..522bbf5a 100644 --- a/api/gateway/aurora/internal/service/gateway/card_payout_store_test.go +++ b/api/gateway/aurora/internal/service/gateway/card_payout_store_test.go @@ -53,7 +53,7 @@ func (s *cardPayoutStore) FindByIdempotencyKey(_ context.Context, key string) (* return v, nil } } - return nil, nil + return nil, nil //nolint:nilnil // test store: payout not found by idempotency key } func (s *cardPayoutStore) FindByOperationRef(_ context.Context, ref string) (*model.CardPayout, error) { @@ -64,7 +64,7 @@ func (s *cardPayoutStore) FindByOperationRef(_ context.Context, ref string) (*mo return v, nil } } - return nil, nil + return nil, nil //nolint:nilnil // test store: payout not found by operation ref } func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model.CardPayout, error) { @@ -75,7 +75,7 @@ func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model. return v, nil } } - return nil, nil + return nil, nil //nolint:nilnil // test store: payout not found by payment id } func (s *cardPayoutStore) Upsert(_ context.Context, record *model.CardPayout) error { diff --git a/api/gateway/aurora/internal/service/gateway/card_processor.go b/api/gateway/aurora/internal/service/gateway/card_processor.go index f79bcaf4..faa40b43 100644 --- a/api/gateway/aurora/internal/service/gateway/card_processor.go +++ b/api/gateway/aurora/internal/service/gateway/card_processor.go @@ -107,7 +107,7 @@ func findOperationRef(operationRef, payoutID string) string { func (p *cardPayoutProcessor) findExistingPayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) { if p == nil || state == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil processor/state means there is no existing payout state to load } if opRef := strings.TrimSpace(state.OperationRef); opRef != "" { existing, err := p.store.Payouts().FindByOperationRef(ctx, opRef) @@ -122,12 +122,12 @@ func (p *cardPayoutProcessor) findExistingPayoutState(ctx context.Context, state } } } - return nil, nil + return nil, nil //nolint:nilnil // nil means no payout state exists for the operation reference } func (p *cardPayoutProcessor) findAndMergePayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) { if p == nil || state == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil processor/state means there is no existing payout state to merge } existing, err := p.findExistingPayoutState(ctx, state) if err != nil { @@ -862,7 +862,7 @@ func (p *cardPayoutProcessor) retryContext() (context.Context, context.CancelFun if timeout <= 0 { return ctx, func() {} } - return context.WithTimeout(ctx, timeout) + return context.WithTimeout(ctx, timeout) //nolint:gosec // cancel func is always invoked by caller } func (p *cardPayoutProcessor) runCardPayoutRetry(req *mntxv1.CardPayoutRequest, attempt uint32, maxAttempts uint32) { @@ -1369,8 +1369,7 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())), ) - cardInput, err := validateCardTokenizeRequest(req, p.config) - if err != nil { + if _, err := validateCardTokenizeRequest(req, p.config); err != nil { p.logger.Warn("Card tokenization validation failed", zap.String("request_id", req.GetRequestId()), zap.String("customer_id", req.GetCustomerId()), @@ -1379,14 +1378,15 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke return nil, err } + var err error + req = sanitizeCardTokenizeRequest(req) + cardInput := extractTokenizeCard(req) + projectID, err := p.resolveProjectID(req.GetProjectId(), "request_id", req.GetRequestId()) if err != nil { return nil, err } - req = sanitizeCardTokenizeRequest(req) - cardInput = extractTokenizeCard(req) - token := buildSimulatedCardToken(req.GetRequestId(), cardInput.pan) maskedPAN := provider.MaskPAN(cardInput.pan) p.rememberTokenPAN(token, cardInput.pan) @@ -1506,7 +1506,8 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt } retryScheduled := false - if state.Status == model.PayoutStatusFailed || state.Status == model.PayoutStatusCancelled { + switch state.Status { + case model.PayoutStatusFailed, model.PayoutStatusCancelled: decision := p.retryPolicy.decideProviderFailure(state.ProviderCode) attemptsUsed := p.currentDispatchAttempt(operationRef) maxAttempts := p.maxDispatchAttempts() @@ -1553,7 +1554,7 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt if !retryScheduled && strings.TrimSpace(state.FailureReason) == "" { state.FailureReason = payoutFailureReason(state.ProviderCode, state.ProviderMessage) } - } else if state.Status == model.PayoutStatusSuccess { + case model.PayoutStatusSuccess: state.FailureReason = "" } diff --git a/api/gateway/aurora/internal/service/gateway/card_processor_test.go b/api/gateway/aurora/internal/service/gateway/card_processor_test.go index 91ff941c..7453b06c 100644 --- a/api/gateway/aurora/internal/service/gateway/card_processor_test.go +++ b/api/gateway/aurora/internal/service/gateway/card_processor_test.go @@ -36,6 +36,15 @@ func (s staticClock) Now() time.Time { return s.now } +func mustMarshalJSON(t *testing.T, value any) []byte { + t.Helper() + body, err := json.Marshal(value) + if err != nil { + t.Fatalf("json marshal failed: %v", err) + } + return body +} + type apiResponse struct { RequestID string `json:"request_id"` Status string `json:"status"` @@ -70,7 +79,7 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) { Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { resp := apiResponse{} resp.Operation.RequestID = "req-123" - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), @@ -260,7 +269,7 @@ func TestCardPayoutProcessor_Submit_SameParentDifferentOperationsStoredSeparatel callN++ resp := apiResponse{} resp.Operation.RequestID = fmt.Sprintf("req-%d", callN) - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), @@ -350,7 +359,7 @@ func TestCardPayoutProcessor_StrictMode_BlocksSecondOperationUntilFirstFinalCall n := callN.Add(1) resp := apiResponse{} resp.Operation.RequestID = fmt.Sprintf("req-%d", n) - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), @@ -544,7 +553,7 @@ func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineUntilSuccess(t *t if n == 1 { resp.Code = providerCodeDeclineAmountOrFrequencyLimit resp.Message = "Decline due to amount or frequency limit" - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusTooManyRequests, Body: io.NopCloser(bytes.NewReader(body)), @@ -552,7 +561,7 @@ func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineUntilSuccess(t *t }, nil } resp.Operation.RequestID = "req-retry-success" - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), @@ -617,7 +626,7 @@ func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineThenFails(t *test Code: providerCodeDeclineAmountOrFrequencyLimit, Message: "Decline due to amount or frequency limit", } - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusTooManyRequests, Body: io.NopCloser(bytes.NewReader(body)), @@ -689,7 +698,7 @@ func TestCardPayoutProcessor_ProcessCallback_RetryableDeclineSchedulesRetry(t *t } else { resp.Operation.RequestID = "req-after-callback-retry" } - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), diff --git a/api/gateway/aurora/internal/service/gateway/card_tokenize_validation_test.go b/api/gateway/aurora/internal/service/gateway/card_tokenize_validation_test.go index e0a51783..31e02531 100644 --- a/api/gateway/aurora/internal/service/gateway/card_tokenize_validation_test.go +++ b/api/gateway/aurora/internal/service/gateway/card_tokenize_validation_test.go @@ -40,8 +40,8 @@ func TestValidateCardTokenizeRequest_Expired(t *testing.T) { cfg := testProviderConfig() req := validCardTokenizeRequest() now := time.Now().UTC() - req.CardExpMonth = uint32(now.Month()) - req.CardExpYear = uint32(now.Year() - 1) + req.CardExpMonth = uint32(now.Month()) //nolint:gosec // month value is bounded by time.Time + req.CardExpYear = uint32(now.Year() - 1) //nolint:gosec // test value intentionally uses previous year _, err := validateCardTokenizeRequest(req, cfg) requireReason(t, err, "expired_card") diff --git a/api/gateway/aurora/internal/service/gateway/connector.go b/api/gateway/aurora/internal/service/gateway/connector.go index 58df3891..8983ad0c 100644 --- a/api/gateway/aurora/internal/service/gateway/connector.go +++ b/api/gateway/aurora/internal/service/gateway/connector.go @@ -251,8 +251,8 @@ func buildCardPayoutRequestFromParams(reader params.Reader, AmountMinor: amountMinor, Currency: currency, CardPan: strings.TrimSpace(reader.String("card_pan")), - CardExpYear: uint32(readerInt64(reader, "card_exp_year")), - CardExpMonth: uint32(readerInt64(reader, "card_exp_month")), + CardExpYear: uint32(readerInt64(reader, "card_exp_year")), //nolint:gosec // values are validated by request validators + CardExpMonth: uint32(readerInt64(reader, "card_exp_month")), //nolint:gosec // values are validated by request validators CardHolder: strings.TrimSpace(reader.String("card_holder")), Metadata: metadataFromReader(reader), OperationRef: operationRef, diff --git a/api/gateway/aurora/internal/service/gateway/payout_execution_mode.go b/api/gateway/aurora/internal/service/gateway/payout_execution_mode.go index b703aff8..73ff6944 100644 --- a/api/gateway/aurora/internal/service/gateway/payout_execution_mode.go +++ b/api/gateway/aurora/internal/service/gateway/payout_execution_mode.go @@ -128,12 +128,13 @@ func (m *strictIsolatedPayoutExecutionMode) tryAcquire(operationRef string) (<-c return nil, false, errPayoutExecutionModeStopped } - switch owner := strings.TrimSpace(m.activeOperation); { - case owner == "": + owner := strings.TrimSpace(m.activeOperation) + switch owner { + case "": m.activeOperation = operationRef m.signalLocked() return nil, true, nil - case owner == operationRef: + case operationRef: return nil, true, nil default: return m.waitCh, false, nil diff --git a/api/gateway/aurora/internal/service/gateway/payout_failure_policy_test.go b/api/gateway/aurora/internal/service/gateway/payout_failure_policy_test.go index d5566f57..3bd381e6 100644 --- a/api/gateway/aurora/internal/service/gateway/payout_failure_policy_test.go +++ b/api/gateway/aurora/internal/service/gateway/payout_failure_policy_test.go @@ -28,7 +28,6 @@ func TestPayoutFailurePolicy_DecideProviderFailure(t *testing.T) { } for _, tc := range cases { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Helper() got := policy.decideProviderFailure(tc.code) diff --git a/api/gateway/aurora/internal/service/gateway/scenario_simulator.go b/api/gateway/aurora/internal/service/gateway/scenario_simulator.go index 39e064d8..3406b306 100644 --- a/api/gateway/aurora/internal/service/gateway/scenario_simulator.go +++ b/api/gateway/aurora/internal/service/gateway/scenario_simulator.go @@ -1,7 +1,7 @@ package gateway import ( - "crypto/sha1" + "crypto/sha256" "encoding/hex" "fmt" "strings" @@ -238,6 +238,6 @@ func normalizeExpiryYear(year uint32) string { func buildSimulatedCardToken(requestID, pan string) string { input := strings.TrimSpace(requestID) + "|" + normalizeCardNumber(pan) - sum := sha1.Sum([]byte(input)) + sum := sha256.Sum256([]byte(input)) return "aur_tok_" + hex.EncodeToString(sum[:8]) } diff --git a/api/gateway/aurora/internal/service/gateway/service.go b/api/gateway/aurora/internal/service/gateway/service.go index 3cdf2003..aaa82c4c 100644 --- a/api/gateway/aurora/internal/service/gateway/service.go +++ b/api/gateway/aurora/internal/service/gateway/service.go @@ -174,7 +174,7 @@ func (s *Service) startDiscoveryAnnouncer() { if strings.TrimSpace(announce.ID) == "" { announce.ID = discovery.StablePaymentGatewayID(discovery.RailCardPayout) } - s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.MntxGateway), announce) + s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.MntxGateway, announce) s.announcer.Start() } diff --git a/api/gateway/aurora/internal/service/gateway/testhelpers_test.go b/api/gateway/aurora/internal/service/gateway/testhelpers_test.go index 4445342f..cb7150f4 100644 --- a/api/gateway/aurora/internal/service/gateway/testhelpers_test.go +++ b/api/gateway/aurora/internal/service/gateway/testhelpers_test.go @@ -18,8 +18,8 @@ func requireReason(t *testing.T, err error, reason string) { if !errors.Is(err, merrors.ErrInvalidArg) { t.Fatalf("expected invalid argument error, got %v", err) } - reasoned, ok := err.(payoutFailure) - if !ok { + var reasoned payoutFailure + if !errors.As(err, &reasoned) { t.Fatalf("expected payout failure reason, got %T", err) } if reasoned.Reason() != reason { @@ -82,5 +82,5 @@ func validCardTokenizeRequest() *mntxv1.CardTokenizeRequest { func futureExpiry() (uint32, uint32) { now := time.Now().UTC() - return uint32(now.Month()), uint32(now.Year() + 1) + return uint32(now.Month()), uint32(now.Year() + 1) //nolint:gosec // month/year values are bounded by time.Time } diff --git a/api/gateway/aurora/internal/service/gateway/transfer_notifications.go b/api/gateway/aurora/internal/service/gateway/transfer_notifications.go index b30a8c5d..c2f16805 100644 --- a/api/gateway/aurora/internal/service/gateway/transfer_notifications.go +++ b/api/gateway/aurora/internal/service/gateway/transfer_notifications.go @@ -65,7 +65,7 @@ func (p *cardPayoutProcessor) updatePayoutStatus(ctx context.Context, state *mod return nil, emitErr } } - return nil, nil + return struct{}{}, nil }) if err != nil { p.logger.Warn("Failed to update transfer status", zap.Error(err), mzap.ObjRef("payout_ref", state.ID), diff --git a/api/gateway/chain/.golangci.yml b/api/gateway/chain/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/gateway/chain/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/gateway/chain/client/client.go b/api/gateway/chain/client/client.go index 2c2babf1..0b5a53be 100644 --- a/api/gateway/chain/client/client.go +++ b/api/gateway/chain/client/client.go @@ -321,11 +321,13 @@ func (c *chainGatewayClient) callContext(ctx context.Context) (context.Context, if timeout <= 0 { timeout = 3 * time.Second } + //nolint:gosec // Caller receives cancel func and defers it in every call path. return context.WithTimeout(ctx, timeout) } func walletParamsFromRequest(req *chainv1.CreateManagedWalletRequest) (*structpb.Struct, error) { if req == nil { + //nolint:nilnil // Nil request means optional params are absent. return nil, nil } params := map[string]interface{}{ diff --git a/api/gateway/chain/internal/keymanager/vault/manager.go b/api/gateway/chain/internal/keymanager/vault/manager.go index 2765acfb..82167019 100644 --- a/api/gateway/chain/internal/keymanager/vault/manager.go +++ b/api/gateway/chain/internal/keymanager/vault/manager.go @@ -31,7 +31,7 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) { } keys, err := managedkey.New(managedkey.Options{ Logger: logger, - Config: managedkey.Config(cfg), + Config: cfg, Component: "vault key manager", DefaultKeyPrefix: "gateway/chain/wallets", }) diff --git a/api/gateway/chain/internal/server/internal/serverimp.go b/api/gateway/chain/internal/server/internal/serverimp.go index 7cf85aa6..d745c28f 100644 --- a/api/gateway/chain/internal/server/internal/serverimp.go +++ b/api/gateway/chain/internal/server/internal/serverimp.go @@ -264,6 +264,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew func buildGasTopUpPolicy(chainName pmodel.ChainNetwork, cfg *gasTopUpPolicyConfig) (*gatewayshared.GasTopUpPolicy, error) { if cfg == nil { + //nolint:nilnil // Nil config means gas top-up policy is intentionally disabled. return nil, nil } defaultRule, defaultSet, err := parseGasTopUpRule(chainName, "default", cfg.gasTopUpRuleConfig) diff --git a/api/gateway/chain/internal/service/gateway/commands/transfer/gas_topup.go b/api/gateway/chain/internal/service/gateway/commands/transfer/gas_topup.go index bdb60ebe..62203bd1 100644 --- a/api/gateway/chain/internal/service/gateway/commands/transfer/gas_topup.go +++ b/api/gateway/chain/internal/service/gateway/commands/transfer/gas_topup.go @@ -223,6 +223,7 @@ func defaultGasTopUp(estimatedFee *moneyv1.Money, currentBalance *moneyv1.Money) } required := estimated.Sub(current) if !required.IsPositive() { + //nolint:nilnil // No top-up required is represented as (nil, nil). return nil, nil } return &moneyv1.Money{ diff --git a/api/gateway/chain/internal/service/gateway/commands/wallet/create.go b/api/gateway/chain/internal/service/gateway/commands/wallet/create.go index 2dfcdfef..3968777f 100644 --- a/api/gateway/chain/internal/service/gateway/commands/wallet/create.go +++ b/api/gateway/chain/internal/service/gateway/commands/wallet/create.go @@ -143,7 +143,7 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C Metadata: metadata, } if description != nil { - wallet.Describable.Description = description + wallet.Description = description } created, err := c.deps.Storage.Wallets().Create(ctx, wallet) diff --git a/api/gateway/chain/internal/service/gateway/connector.go b/api/gateway/chain/internal/service/gateway/connector.go index d936f8d2..a61d9fb5 100644 --- a/api/gateway/chain/internal/service/gateway/connector.go +++ b/api/gateway/chain/internal/service/gateway/connector.go @@ -42,19 +42,19 @@ func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilit func (s *Service) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) { if req == nil { - return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil)}, nil } if req.GetKind() != connectorv1.AccountKind_CHAIN_MANAGED_WALLET { - return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil)}, nil } reader := params.New(req.GetParams()) orgRef := strings.TrimSpace(reader.String("organization_ref")) if orgRef == "" { - return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref is required", nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref is required", nil)}, nil } asset, err := parseChainAsset(strings.TrimSpace(req.GetAsset()), reader) if err != nil { - return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil)}, nil } resp, err := s.CreateManagedWallet(ctx, &chainv1.CreateManagedWalletRequest{ @@ -66,7 +66,7 @@ func (s *Service) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountR Describable: describableFromLabel(req.GetLabel(), reader.String("description")), }) if err != nil { - return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil)}, nil } return &connectorv1.OpenAccountResponse{Account: chainWalletToAccount(resp.GetWallet())}, nil } @@ -136,32 +136,32 @@ func (s *Service) GetBalance(ctx context.Context, req *connectorv1.GetBalanceReq func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) { if req == nil || req.GetOperation() == nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil)}}, nil } op := req.GetOperation() if strings.TrimSpace(op.GetIdempotencyKey()) == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op)}}, nil } reader := params.New(op.GetParams()) orgRef := strings.TrimSpace(reader.String("organization_ref")) source := operationAccountID(op.GetFrom()) if source == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "operation: from.account is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "operation: from.account is required", op)}}, nil } switch op.GetType() { case connectorv1.OperationType_TRANSFER: dest, err := transferDestinationFromOperation(op) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil } amount := op.GetMoney() if amount == nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op)}}, nil } amount = normalizeMoneyForChain(amount) if orgRef == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: organization_ref is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: organization_ref is required", op)}}, nil } resp, err := s.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{ IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()), @@ -176,7 +176,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp PaymentRef: strings.TrimSpace(reader.String("payment_ref")), }) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil } transfer := resp.GetTransfer() return &connectorv1.SubmitOperationResponse{ @@ -189,11 +189,11 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp case connectorv1.OperationType_FEE_ESTIMATE: dest, err := transferDestinationFromOperation(op) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil } amount := op.GetMoney() if amount == nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "estimate: money is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "estimate: money is required", op)}}, nil } amount = normalizeMoneyForChain(amount) opID := strings.TrimSpace(op.GetOperationId()) @@ -206,7 +206,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp Amount: amount, }) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil } result := feeEstimateResult(resp) return &connectorv1.SubmitOperationResponse{ @@ -219,7 +219,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp case connectorv1.OperationType_GAS_TOPUP: fee, err := parseMoneyFromMap(reader.Map("estimated_total_fee")) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil } fee = normalizeMoneyForChain(fee) mode := strings.ToLower(strings.TrimSpace(reader.String("mode"))) @@ -237,7 +237,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp EstimatedTotalFee: fee, }) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil } return &connectorv1.SubmitOperationResponse{ Receipt: &connectorv1.OperationReceipt{ @@ -252,11 +252,11 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp opID = strings.TrimSpace(op.GetIdempotencyKey()) } if orgRef == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: organization_ref is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: organization_ref is required", op)}}, nil } target := strings.TrimSpace(reader.String("target_wallet_ref")) if target == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: target_wallet_ref is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: target_wallet_ref is required", op)}}, nil } resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{ IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()), @@ -270,7 +270,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp PaymentRef: strings.TrimSpace(reader.String("payment_ref")), }) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil } transferRef := "" if transfer := resp.GetTransfer(); transfer != nil { @@ -284,10 +284,10 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp }, }, nil default: - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: invalid mode", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: invalid mode", op)}}, nil } default: - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op)}}, nil } } @@ -722,11 +722,10 @@ func structFromMap(values map[string]interface{}) *structpb.Struct { return result } -func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError { +func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation) *connectorv1.ConnectorError { err := &connectorv1.ConnectorError{ - Code: code, - Message: strings.TrimSpace(message), - AccountId: strings.TrimSpace(accountID), + Code: code, + Message: strings.TrimSpace(message), } if op != nil { err.CorrelationId = strings.TrimSpace(op.GetCorrelationId()) diff --git a/api/gateway/chain/internal/service/gateway/driver/evm/evm.go b/api/gateway/chain/internal/service/gateway/driver/evm/evm.go index e9398c8b..51882f36 100644 --- a/api/gateway/chain/internal/service/gateway/driver/evm/evm.go +++ b/api/gateway/chain/internal/service/gateway/driver/evm/evm.go @@ -297,7 +297,7 @@ func EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, GasPrice: gasPrice, Value: amountBase, } - gasLimit, err := estimateGas(timeoutCtx, network, client, rpcClient, callMsg) + gasLimit, err := estimateGas(timeoutCtx, client, callMsg) if err != nil { logger.Warn("Failed to estimate gas", zap.Error(err), zap.Any("call_mesasge", callMsg)) return nil, merrors.Internal("failed to estimate gas: " + err.Error()) @@ -345,7 +345,7 @@ func EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, GasPrice: gasPrice, Data: input, } - gasLimit, err := estimateGas(timeoutCtx, network, client, rpcClient, callMsg) + gasLimit, err := estimateGas(timeoutCtx, client, callMsg) if err != nil { logger.Warn("Failed to estimate gas", zap.Error(err), zap.Any("call_message", callMsg)) return nil, merrors.Internal("failed to estimate gas: " + err.Error()) @@ -456,7 +456,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ GasPrice: gasPrice, Value: amountInt, } - gasLimit, err := estimateGas(ctx, network, client, rpcClient, callMsg) + gasLimit, err := estimateGas(ctx, client, callMsg) if err != nil { logger.Warn("Failed to estimate gas", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef), @@ -504,7 +504,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ GasPrice: gasPrice, Data: input, } - gasLimit, err := estimateGas(ctx, network, client, rpcClient, callMsg) + gasLimit, err := estimateGas(ctx, client, callMsg) if err != nil { logger.Warn("Failed to estimate gas", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef), @@ -653,26 +653,10 @@ type gasEstimator interface { EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) } -func estimateGas(ctx context.Context, network shared.Network, client gasEstimator, rpcClient *rpc.Client, callMsg ethereum.CallMsg) (uint64, error) { +func estimateGas(ctx context.Context, client gasEstimator, callMsg ethereum.CallMsg) (uint64, error) { return client.EstimateGas(ctx, callMsg) } -func estimateGasTron(ctx context.Context, rpcClient *rpc.Client, callMsg ethereum.CallMsg) (uint64, error) { - call := tronEstimateCall(callMsg) - var hexResp string - if err := rpcClient.CallContext(ctx, &hexResp, "eth_estimateGas", call); err != nil { - return 0, err - } - val, err := shared.DecodeHexBig(hexResp) - if err != nil { - return 0, err - } - if val == nil { - return 0, merrors.Internal("failed to decode gas estimate") - } - return val.Uint64(), nil -} - func tronEstimateCall(callMsg ethereum.CallMsg) map[string]string { call := make(map[string]string) if callMsg.From != (common.Address{}) { diff --git a/api/gateway/chain/internal/service/gateway/outbox_reliable.go b/api/gateway/chain/internal/service/gateway/outbox_reliable.go index d365c4b9..b7452e75 100644 --- a/api/gateway/chain/internal/service/gateway/outbox_reliable.go +++ b/api/gateway/chain/internal/service/gateway/outbox_reliable.go @@ -24,15 +24,15 @@ func (s *Service) outboxStore() gatewayoutbox.Store { return provider.Outbox() } -func (s *Service) startOutboxReliableProducer() error { +func (s *Service) startOutboxReliableProducer(_ context.Context) error { if s == nil || s.storage == nil { return nil } - return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg) + return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg) //nolint:contextcheck // Reliable runtime start API does not accept context. } func (s *Service) sendWithOutbox(ctx context.Context, env me.Envelope) error { - if err := s.startOutboxReliableProducer(); err != nil { + if err := s.startOutboxReliableProducer(ctx); err != nil { return err } return s.outbox.Send(ctx, env) diff --git a/api/gateway/chain/internal/service/gateway/rpcclient/clients.go b/api/gateway/chain/internal/service/gateway/rpcclient/clients.go index effb9672..787acf5c 100644 --- a/api/gateway/chain/internal/service/gateway/rpcclient/clients.go +++ b/api/gateway/chain/internal/service/gateway/rpcclient/clients.go @@ -163,7 +163,9 @@ func (l *loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro } bodyBytes, _ := io.ReadAll(resp.Body) - resp.Body.Close() + if closeErr := resp.Body.Close(); closeErr != nil { + l.logger.Warn("Failed to close RPC response body", append(fields, zap.Error(closeErr))...) + } resp.Body = io.NopCloser(strings.NewReader(string(bodyBytes))) respFields := append(fields, diff --git a/api/gateway/chain/internal/service/gateway/service.go b/api/gateway/chain/internal/service/gateway/service.go index 446ac324..5c545179 100644 --- a/api/gateway/chain/internal/service/gateway/service.go +++ b/api/gateway/chain/internal/service/gateway/service.go @@ -89,7 +89,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro } svc.settings = svc.settings.withDefaults() svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients) - if err := svc.startOutboxReliableProducer(); err != nil { + if err := svc.startOutboxReliableProducer(context.Background()); err != nil { svc.logger.Warn("Failed to initialise outbox reliable producer", zap.Error(err)) } @@ -231,7 +231,7 @@ func (s *Service) startDiscoveryAnnouncers() { InvokeURI: s.invokeURI, Version: version, } - announcer := discovery.NewAnnouncer(s.logger, s.producer, string(mservice.ChainGateway), announce) + announcer := discovery.NewAnnouncer(s.logger, s.producer, mservice.ChainGateway, announce) announcer.Start() s.announcers = append(s.announcers, announcer) } diff --git a/api/gateway/chain/internal/service/gateway/service_test.go b/api/gateway/chain/internal/service/gateway/service_test.go index 32c03d5b..3346bf27 100644 --- a/api/gateway/chain/internal/service/gateway/service_test.go +++ b/api/gateway/chain/internal/service/gateway/service_test.go @@ -272,7 +272,7 @@ type walletsNoDataRepository struct { } func (r *walletsNoDataRepository) Wallets() storage.WalletsStore { - return &walletsNoDataStore{WalletsStore: r.inMemoryRepository.wallets} + return &walletsNoDataStore{WalletsStore: r.wallets} } type walletsNoDataStore struct { @@ -673,10 +673,11 @@ func sanitizeLimit(requested int32, def, max int64) int64 { if requested <= 0 { return def } - if requested > int32(max) { + requested64 := int64(requested) + if requested64 > max { return max } - return int64(requested) + return requested64 } func newTestService(t *testing.T) (*Service, *inMemoryRepository) { diff --git a/api/gateway/chain/internal/service/gateway/shared/hex.go b/api/gateway/chain/internal/service/gateway/shared/hex.go index e33ac582..6dda2155 100644 --- a/api/gateway/chain/internal/service/gateway/shared/hex.go +++ b/api/gateway/chain/internal/service/gateway/shared/hex.go @@ -1,6 +1,7 @@ package shared import ( + "math" "math/big" "strings" @@ -46,5 +47,9 @@ func DecodeHexUint8(input string) (uint8, error) { if val.BitLen() > 8 { return 0, errHexOutOfRange } - return uint8(val.Uint64()), nil + decoded := val.Uint64() + if decoded > math.MaxUint8 { + return 0, errHexOutOfRange + } + return uint8(decoded), nil } diff --git a/api/gateway/chain/internal/service/gateway/transfer_execution.go b/api/gateway/chain/internal/service/gateway/transfer_execution.go index 11152555..74b880c2 100644 --- a/api/gateway/chain/internal/service/gateway/transfer_execution.go +++ b/api/gateway/chain/internal/service/gateway/transfer_execution.go @@ -41,26 +41,26 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet return err } - if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusProcessing, "", ""); err != nil { + if err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusProcessing, "", ""); err != nil { s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err)) } driverDeps := s.driverDeps() chainDriver, err := s.driverForNetwork(network.Name) if err != nil { - _, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") + _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") return err } destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination) if err != nil { - _, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") + _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") return err } sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress) if err != nil { - _, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") + _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") return err } if chainDriver.Name() == "tron" && sourceAddress == destinationAddress { @@ -69,7 +69,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet zap.String("wallet_ref", sourceWalletRef), zap.String("network", string(network.Name)), ) - if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusSuccess, "", ""); err != nil { + if err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusSuccess, "", ""); err != nil { s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err)) } return nil @@ -78,13 +78,13 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress) if err != nil { s.logger.Warn("Failed to submit transfer", zap.String("transfer_ref", transferRef), zap.Error(err)) - if _, e := s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), ""); e != nil { + if e := s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), ""); e != nil { s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(e)) } return err } - if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusWaiting, "", txHash); err != nil { + if err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusWaiting, "", txHash); err != nil { s.logger.Warn("Failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err)) } @@ -104,7 +104,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet failureReason = "transaction reverted" pStatus = model.TransferStatusFailed } - if _, err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil { + if err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil { s.logger.Warn("Failed to update transfer status", zap.Error(err), zap.String("transfer_ref", transferRef), zap.String("status", string(pStatus))) } diff --git a/api/gateway/chain/internal/service/gateway/transfer_notifications.go b/api/gateway/chain/internal/service/gateway/transfer_notifications.go index 4e63236f..851e9043 100644 --- a/api/gateway/chain/internal/service/gateway/transfer_notifications.go +++ b/api/gateway/chain/internal/service/gateway/transfer_notifications.go @@ -57,16 +57,16 @@ func toError(t *model.Transfer) string { return t.FailureReason } -func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) { +func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) error { if !isFinalTransferStatus(status) { - transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash) + _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash) if err != nil { s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err)) } - return transfer, err + return err } - res, err := s.executeTransaction(ctx, func(txCtx context.Context) (any, error) { + _, err := s.executeTransaction(ctx, func(txCtx context.Context) (any, error) { transfer, statusErr := s.storage.Transfers().UpdateStatus(txCtx, transferRef, status, failureReason, txHash) if statusErr != nil { return nil, statusErr @@ -80,11 +80,9 @@ func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, }) if err != nil { s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err)) - return nil, err + return err } - - transfer, _ := res.(*model.Transfer) - return transfer, nil + return nil } func (s *Service) emitTransferStatusEvent(ctx context.Context, transfer *model.Transfer) error { diff --git a/api/gateway/chain/storage/model/wallet.go b/api/gateway/chain/storage/model/wallet.go index 1bc9d18c..c9e7a0e8 100644 --- a/api/gateway/chain/storage/model/wallet.go +++ b/api/gateway/chain/storage/model/wallet.go @@ -6,7 +6,6 @@ import ( "github.com/tech/sendico/pkg/db/storable" pkgmodel "github.com/tech/sendico/pkg/model" - pmodel "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" ) @@ -67,7 +66,7 @@ type ManagedWalletFilter struct { // - pointer to empty string: filter for wallets where owner_ref is empty // - pointer to a value: filter for wallets where owner_ref matches OwnerRefFilter *string - Network pmodel.ChainNetwork + Network pkgmodel.ChainNetwork TokenSymbol string Cursor string Limit int32 diff --git a/api/gateway/chain/storage/mongo/store/wallets.go b/api/gateway/chain/storage/mongo/store/wallets.go index d4b44f12..26f7a8c5 100644 --- a/api/gateway/chain/storage/mongo/store/wallets.go +++ b/api/gateway/chain/storage/mongo/store/wallets.go @@ -186,7 +186,6 @@ func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (* if listErr != nil { if errors.Is(listErr, merrors.ErrNoData) { wallets = make([]model.ManagedWallet, 0) - listErr = nil } else { w.logger.Warn("Wallet list failed", append(fields, zap.Error(listErr))...) return nil, listErr diff --git a/api/gateway/chsettle/.golangci.yml b/api/gateway/chsettle/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/gateway/chsettle/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/gateway/chsettle/internal/service/gateway/confirmation_flow.go b/api/gateway/chsettle/internal/service/gateway/confirmation_flow.go index 2d09c0e8..68e7048b 100644 --- a/api/gateway/chsettle/internal/service/gateway/confirmation_flow.go +++ b/api/gateway/chsettle/internal/service/gateway/confirmation_flow.go @@ -217,7 +217,7 @@ func (s *Service) onTelegramUpdate(ctx context.Context, update *model.TelegramWe ReplyToMessageID: message.MessageID, Text: "Only approved users can confirm this payment.", }); e != nil { - s.logger.Warn("Failed to create telegram text", append(replyFields, zap.Error(err))...) + s.logger.Warn("Failed to create telegram text", append(replyFields, zap.Error(e))...) } if err := s.clearPendingConfirmation(ctx, pending.RequestID); err != nil { return err @@ -307,13 +307,13 @@ func (s *Service) publishPendingConfirmationResult(pending *storagemodel.Pending } sourceService := strings.TrimSpace(pending.SourceService) if sourceService == "" { - sourceService = string(mservice.PaymentGateway) + sourceService = mservice.PaymentGateway } rail := strings.TrimSpace(pending.Rail) if rail == "" { rail = s.rail } - env := confirmations.ConfirmationResult(string(mservice.PaymentGateway), result, sourceService, rail) + env := confirmations.ConfirmationResult(mservice.PaymentGateway, result, sourceService, rail) if err := s.producer.SendMessage(env); err != nil { s.logger.Warn("Failed to publish confirmation result", zap.Error(err), zap.String("request_id", strings.TrimSpace(result.RequestID)), @@ -338,7 +338,7 @@ func (s *Service) sendTelegramText(_ context.Context, request *model.TelegramTex if request.ChatID == "" || request.Text == "" { return merrors.InvalidArgument("telegram chat_id and text are required", "chat_id", "text") } - env := tnotifications.TelegramText(string(mservice.PaymentGateway), request) + env := tnotifications.TelegramText(mservice.PaymentGateway, request) if err := s.producer.SendMessage(env); err != nil { s.logger.Warn("Failed to publish telegram text request", zap.Error(err), zap.String("request_id", request.RequestID), diff --git a/api/gateway/chsettle/internal/service/gateway/scenario_simulator.go b/api/gateway/chsettle/internal/service/gateway/scenario_simulator.go index e5c411b6..0ba0841c 100644 --- a/api/gateway/chsettle/internal/service/gateway/scenario_simulator.go +++ b/api/gateway/chsettle/internal/service/gateway/scenario_simulator.go @@ -228,9 +228,7 @@ func amountModuloSlot(amount *paymenttypes.Money) (int, bool) { return 0, false } sign := 1 - if strings.HasPrefix(raw, "+") { - raw = strings.TrimPrefix(raw, "+") - } + raw = strings.TrimPrefix(raw, "+") if strings.HasPrefix(raw, "-") { sign = -1 raw = strings.TrimPrefix(raw, "-") @@ -294,7 +292,7 @@ func hashModulo(input string, mod int) int { } h := fnv.New32a() _, _ = h.Write([]byte(strings.TrimSpace(input))) - return int(h.Sum32() % uint32(mod)) + return int(h.Sum32()) % mod } func (s settlementScenario) delayedTransitionEnabled() bool { diff --git a/api/gateway/chsettle/internal/service/gateway/service.go b/api/gateway/chsettle/internal/service/gateway/service.go index 9f1fd8df..7bef184c 100644 --- a/api/gateway/chsettle/internal/service/gateway/service.go +++ b/api/gateway/chsettle/internal/service/gateway/service.go @@ -24,7 +24,6 @@ import ( tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" - pmodel "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" paymenttypes "github.com/tech/sendico/pkg/payments/types" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" @@ -63,7 +62,7 @@ type Config struct { AcceptedUserIDs []string SuccessReaction string InvokeURI string - MessagingSettings pmodel.SettingsT + MessagingSettings model.SettingsT DiscoveryRegistry *discovery.Registry Treasury TreasuryConfig } @@ -90,7 +89,7 @@ type Service struct { producer msg.Producer broker mb.Broker cfg Config - msgCfg pmodel.SettingsT + msgCfg model.SettingsT rail string chatID string announcer *discovery.Announcer @@ -249,9 +248,9 @@ func (s *Service) startConsumers() { } return } - resultProcessor := confirmations.NewConfirmationResultProcessor(s.logger, string(mservice.PaymentGateway), s.rail, s.onConfirmationResult) + resultProcessor := confirmations.NewConfirmationResultProcessor(s.logger, mservice.PaymentGateway, s.rail, s.onConfirmationResult) s.consumeProcessor(resultProcessor) - dispatchProcessor := confirmations.NewConfirmationDispatchProcessor(s.logger, string(mservice.PaymentGateway), s.rail, s.onConfirmationDispatch) + dispatchProcessor := confirmations.NewConfirmationDispatchProcessor(s.logger, mservice.PaymentGateway, s.rail, s.onConfirmationDispatch) s.consumeProcessor(dispatchProcessor) updateProcessor := tnotifications.NewTelegramUpdateProcessor(s.logger, s.onTelegramUpdate) s.consumeProcessor(updateProcessor) @@ -573,7 +572,7 @@ func (s *Service) buildConfirmationRequest(intent *model.PaymentGatewayIntent) ( QuoteRef: intent.QuoteRef, AcceptedUserIDs: s.cfg.AcceptedUserIDs, TimeoutSeconds: timeout, - SourceService: string(mservice.PaymentGateway), + SourceService: mservice.PaymentGateway, Rail: rail, OperationRef: intent.OperationRef, IntentRef: intent.IntentRef, @@ -590,7 +589,7 @@ func (s *Service) sendConfirmationRequest(request *model.ConfirmationRequest) er s.logger.Warn("Messaging producer not configured") return merrors.Internal("messaging producer is not configured") } - env := confirmations.ConfirmationRequest(string(mservice.PaymentGateway), request) + env := confirmations.ConfirmationRequest(mservice.PaymentGateway, request) if err := s.producer.SendMessage(env); err != nil { s.logger.Warn("Failed to publish confirmation request", zap.Error(err), @@ -626,7 +625,7 @@ func (s *Service) publishTelegramReaction(result *model.ConfirmationResult) { if request.ChatID == "" || request.MessageID == "" || request.Emoji == "" { return } - env := tnotifications.TelegramReaction(string(mservice.PaymentGateway), request) + env := tnotifications.TelegramReaction(mservice.PaymentGateway, request) if err := s.producer.SendMessage(env); err != nil { s.logger.Warn("Failed to publish telegram reaction", zap.Error(err), @@ -805,7 +804,7 @@ func (s *Service) startAnnouncer() { Operations: caps, InvokeURI: s.invokeURI, } - s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.PaymentGateway), announce) + s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.PaymentGateway, announce) s.announcer.Start() s.logger.Info("Discovery announcer started", zap.String("service", mservice.ChSettle), @@ -910,14 +909,10 @@ func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, d return nil, merrors.InvalidArgument("submit_transfer: operation_ref is required") } quoteRef := strings.TrimSpace(metadata[metadataQuoteRef]) - targetChatID := strings.TrimSpace(metadata[metadataTargetChatID]) outgoingLeg := normalizeRail(metadata[metadataOutgoingLeg]) if outgoingLeg == "" { outgoingLeg = normalizeRail(defaultRail) } - if targetChatID == "" { - targetChatID = strings.TrimSpace(defaultChatID) - } return &model.PaymentGatewayIntent{ PaymentRef: paymentRef, PaymentIntentID: paymentIntentID, diff --git a/api/gateway/chsettle/internal/service/gateway/service_test.go b/api/gateway/chsettle/internal/service/gateway/service_test.go index a28e57e4..77951e15 100644 --- a/api/gateway/chsettle/internal/service/gateway/service_test.go +++ b/api/gateway/chsettle/internal/service/gateway/service_test.go @@ -32,7 +32,7 @@ func (f *fakePaymentsStore) FindByIdempotencyKey(_ context.Context, key string) f.mu.Lock() defer f.mu.Unlock() if f.records == nil { - return nil, nil + return nil, nil //nolint:nilnil // fake store: no records means no payment by idempotency key } return f.records[key], nil } @@ -41,14 +41,14 @@ func (f *fakePaymentsStore) FindByOperationRef(_ context.Context, key string) (* f.mu.Lock() defer f.mu.Unlock() if f.records == nil { - return nil, nil + return nil, nil //nolint:nilnil // fake store: no records means no payment by operation ref } for _, record := range f.records { if record != nil && record.OperationRef == key { return record, nil } } - return nil, nil + return nil, nil //nolint:nilnil // fake store: operation ref not found } func (f *fakePaymentsStore) Upsert(_ context.Context, record *storagemodel.PaymentRecord) error { @@ -124,7 +124,7 @@ func (f *fakePendingStore) FindByRequestID(_ context.Context, requestID string) f.mu.Lock() defer f.mu.Unlock() if f.records == nil { - return nil, nil + return nil, nil //nolint:nilnil // fake store: no pending confirmations configured } return f.records[requestID], nil } @@ -137,7 +137,7 @@ func (f *fakePendingStore) FindByMessageID(_ context.Context, messageID string) return record, nil } } - return nil, nil + return nil, nil //nolint:nilnil // fake store: message id not found } func (f *fakePendingStore) MarkClarified(_ context.Context, requestID string) error { @@ -197,7 +197,7 @@ func (f *fakeBroker) Publish(_ envelope.Envelope) error { } func (f *fakeBroker) Subscribe(event model.NotificationEvent) (<-chan envelope.Envelope, error) { - return nil, nil + return nil, nil //nolint:nilnil // fake broker does not emit events in unit tests } func (f *fakeBroker) Unsubscribe(event model.NotificationEvent, subChan <-chan envelope.Envelope) error { @@ -237,7 +237,7 @@ func newTestService(_ *testing.T) (*Service, *fakeRepo, *captureProducer) { pending: &fakePendingStore{}, } - sigEnv := tnotifications.TelegramReaction(string(mservice.PaymentGateway), &model.TelegramReactionRequest{ + sigEnv := tnotifications.TelegramReaction(mservice.PaymentGateway, &model.TelegramReactionRequest{ RequestID: "x", ChatID: "1", MessageID: "2", diff --git a/api/gateway/chsettle/internal/service/gateway/transfer_notifications.go b/api/gateway/chsettle/internal/service/gateway/transfer_notifications.go index 39fe76eb..6cd24f32 100644 --- a/api/gateway/chsettle/internal/service/gateway/transfer_notifications.go +++ b/api/gateway/chsettle/internal/service/gateway/transfer_notifications.go @@ -64,7 +64,7 @@ func (s *Service) updateTransferStatus(ctx context.Context, record *model.Paymen return nil, emitErr } } - return nil, nil + return struct{}{}, nil }) if err != nil { s.logger.Warn("Failed to update transfer status", zap.String("payment_ref", record.PaymentIntentID), zap.String("status", string(record.Status)), zap.Error(err)) diff --git a/api/gateway/chsettle/internal/service/treasury/bot/router.go b/api/gateway/chsettle/internal/service/treasury/bot/router.go index af338a72..33f7d51a 100644 --- a/api/gateway/chsettle/internal/service/treasury/bot/router.go +++ b/api/gateway/chsettle/internal/service/treasury/bot/router.go @@ -267,7 +267,8 @@ func (r *Router) captureAmount(ctx context.Context, userID, accountID, chatID st }) return } - if typed, ok := err.(limitError); ok { + var typed limitError + if errors.As(err, &typed) { switch typed.LimitKind() { case "per_operation": _ = r.sendText(ctx, chatID, "*Amount exceeds allowed limit*\n\n*Max per operation:* "+markdownCode(typed.LimitMax())+"\n\nEnter another amount or "+markdownCommand(CommandCancel)+".") diff --git a/api/gateway/chsettle/internal/service/treasury/bot/router_test.go b/api/gateway/chsettle/internal/service/treasury/bot/router_test.go index a956fc67..1bab2786 100644 --- a/api/gateway/chsettle/internal/service/treasury/bot/router_test.go +++ b/api/gateway/chsettle/internal/service/treasury/bot/router_test.go @@ -22,7 +22,7 @@ func (f fakeUserBindingResolver) ResolveUserBinding(_ context.Context, telegramU return nil, f.err } if f.bindings == nil { - return nil, nil + return nil, nil //nolint:nilnil // test resolver: missing bindings means user is unknown } return f.bindings[telegramUserID], nil } @@ -36,7 +36,7 @@ func (fakeService) MaxPerOperationLimit() string { } func (fakeService) GetActiveRequestForAccount(context.Context, string) (*storagemodel.TreasuryRequest, error) { - return nil, nil + return nil, nil //nolint:nilnil // test service: no active request by default } func (fakeService) GetAccountProfile(_ context.Context, ledgerAccountID string) (*AccountProfile, error) { @@ -48,15 +48,15 @@ func (fakeService) GetAccountProfile(_ context.Context, ledgerAccountID string) } func (fakeService) CreateRequest(context.Context, CreateRequestInput) (*storagemodel.TreasuryRequest, error) { - return nil, nil + return nil, nil //nolint:nilnil // test service: request creation is not exercised in these tests } func (fakeService) ConfirmRequest(context.Context, string, string) (*storagemodel.TreasuryRequest, error) { - return nil, nil + return nil, nil //nolint:nilnil // test service: confirmation path is not exercised in these tests } func (fakeService) CancelRequest(context.Context, string, string) (*storagemodel.TreasuryRequest, error) { - return nil, nil + return nil, nil //nolint:nilnil // test service: cancellation path is not exercised in these tests } func TestRouterUnauthorizedInAllowedChatSendsAccessDenied(t *testing.T) { diff --git a/api/gateway/chsettle/internal/service/treasury/ledger/client.go b/api/gateway/chsettle/internal/service/treasury/ledger/client.go index 91bde6ac..31cba69c 100644 --- a/api/gateway/chsettle/internal/service/treasury/ledger/client.go +++ b/api/gateway/chsettle/internal/service/treasury/ledger/client.go @@ -260,7 +260,7 @@ func (c *connectorClient) callContext(ctx context.Context) (context.Context, con if ctx == nil { ctx = context.Background() } - return context.WithTimeout(ctx, c.cfg.Timeout) + return context.WithTimeout(ctx, c.cfg.Timeout) //nolint:gosec // cancel func is always invoked by call sites } func structFromMap(values map[string]any) *structpb.Struct { diff --git a/api/gateway/chsettle/internal/service/treasury/ledger/discovery_client.go b/api/gateway/chsettle/internal/service/treasury/ledger/discovery_client.go index 1bee6a1d..d6b40213 100644 --- a/api/gateway/chsettle/internal/service/treasury/ledger/discovery_client.go +++ b/api/gateway/chsettle/internal/service/treasury/ledger/discovery_client.go @@ -141,7 +141,7 @@ func (c *discoveryClient) resolveClient(_ context.Context) (Client, error) { c.endpointKey = key if c.logger != nil { c.logger.Info("Discovered ledger endpoint selected", - zap.String("service", string(mservice.Ledger)), + zap.String("service", mservice.Ledger), zap.String("invoke_uri", endpoint.raw), zap.String("address", endpoint.address), zap.Bool("insecure", endpoint.insecure)) @@ -186,10 +186,10 @@ func (c *discoveryClient) resolveEndpoint() (discoveryEndpoint, error) { func matchesService(service string, candidate mservice.Type) bool { service = strings.TrimSpace(service) - if service == "" || strings.TrimSpace(string(candidate)) == "" { + if service == "" || strings.TrimSpace(candidate) == "" { return false } - return strings.EqualFold(service, strings.TrimSpace(string(candidate))) + return strings.EqualFold(service, strings.TrimSpace(candidate)) } func parseDiscoveryEndpoint(raw string) (discoveryEndpoint, error) { diff --git a/api/gateway/chsettle/internal/service/treasury/module.go b/api/gateway/chsettle/internal/service/treasury/module.go index 462e11c8..8683ea90 100644 --- a/api/gateway/chsettle/internal/service/treasury/module.go +++ b/api/gateway/chsettle/internal/service/treasury/module.go @@ -106,7 +106,7 @@ func (a *botUsersAdapter) ResolveUserBinding(ctx context.Context, telegramUserID return nil, err } if record == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil means no treasury user binding configured } return &bot.UserBinding{ TelegramUserID: strings.TrimSpace(record.TelegramUserID), @@ -145,7 +145,7 @@ func (a *botServiceAdapter) GetAccountProfile(ctx context.Context, ledgerAccount return nil, err } if profile == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil means no account profile is available } return &bot.AccountProfile{ AccountID: strings.TrimSpace(profile.AccountID), diff --git a/api/gateway/chsettle/internal/service/treasury/service.go b/api/gateway/chsettle/internal/service/treasury/service.go index 696b8e93..1ea604d4 100644 --- a/api/gateway/chsettle/internal/service/treasury/service.go +++ b/api/gateway/chsettle/internal/service/treasury/service.go @@ -298,33 +298,33 @@ func (s *Service) ExecuteRequest(ctx context.Context, requestID string) (*Execut return nil, err } if record == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil means request does not exist } switch record.Status { case storagemodel.TreasuryRequestStatusExecuted, storagemodel.TreasuryRequestStatusCancelled, storagemodel.TreasuryRequestStatusFailed: - return nil, nil + return nil, nil //nolint:nilnil // nil means request is terminal and not executable case storagemodel.TreasuryRequestStatusScheduled: claimed, err := s.repo.ClaimScheduled(ctx, requestID) if err != nil { return nil, err } if !claimed { - return nil, nil + return nil, nil //nolint:nilnil // nil means scheduled request is not yet claimable } record, err = s.repo.FindByRequestID(ctx, requestID) if err != nil { return nil, err } if record == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil means request disappeared after claim } } if record.Status != storagemodel.TreasuryRequestStatusConfirmed { - return nil, nil + return nil, nil //nolint:nilnil // nil means request is not ready for execution } return s.executeClaimed(ctx, record) } diff --git a/api/gateway/chsettle/storage/mongo/store/payments.go b/api/gateway/chsettle/storage/mongo/store/payments.go index 22f4e453..d011b13f 100644 --- a/api/gateway/chsettle/storage/mongo/store/payments.go +++ b/api/gateway/chsettle/storage/mongo/store/payments.go @@ -68,7 +68,7 @@ func (p *Payments) FindByIdempotencyKey(ctx context.Context, key string) (*model var result model.PaymentRecord err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldIdempotencyKey, key), &result) if errors.Is(err, merrors.ErrNoData) { - return nil, nil + return nil, nil //nolint:nilnil // nil means no payment found for idempotency key } if err != nil { if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { @@ -87,7 +87,7 @@ func (p *Payments) FindByOperationRef(ctx context.Context, key string) (*model.P var result model.PaymentRecord err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldOperationRef, key), &result) if errors.Is(err, merrors.ErrNoData) { - return nil, nil + return nil, nil //nolint:nilnil // nil means no payment found for operation reference } if err != nil { if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { diff --git a/api/gateway/chsettle/storage/mongo/store/pending_confirmations.go b/api/gateway/chsettle/storage/mongo/store/pending_confirmations.go index 8a361684..82b03eec 100644 --- a/api/gateway/chsettle/storage/mongo/store/pending_confirmations.go +++ b/api/gateway/chsettle/storage/mongo/store/pending_confirmations.go @@ -114,7 +114,7 @@ func (p *PendingConfirmations) FindByRequestID(ctx context.Context, requestID st var result model.PendingConfirmation err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldPendingRequestID, requestID), &result) if errors.Is(err, merrors.ErrNoData) { - return nil, nil + return nil, nil //nolint:nilnil // nil means pending confirmation does not exist } if err != nil { return nil, err @@ -130,7 +130,7 @@ func (p *PendingConfirmations) FindByMessageID(ctx context.Context, messageID st var result model.PendingConfirmation err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldPendingMessageID, messageID), &result) if errors.Is(err, merrors.ErrNoData) { - return nil, nil + return nil, nil //nolint:nilnil // nil means pending confirmation does not exist } if err != nil { return nil, err diff --git a/api/gateway/chsettle/storage/mongo/store/treasury_requests.go b/api/gateway/chsettle/storage/mongo/store/treasury_requests.go index a803144f..b13922f5 100644 --- a/api/gateway/chsettle/storage/mongo/store/treasury_requests.go +++ b/api/gateway/chsettle/storage/mongo/store/treasury_requests.go @@ -165,7 +165,7 @@ func (t *TreasuryRequests) FindByRequestID(ctx context.Context, requestID string err := t.repo.FindOneByFilter(ctx, repository.Filter(fieldTreasuryRequestID, requestID), &result) if errors.Is(err, merrors.ErrNoData) { t.logger.Debug("Treasury request not found", zap.String("request_id", requestID)) - return nil, nil + return nil, nil //nolint:nilnil // nil means treasury request does not exist } if err != nil { t.logger.Warn("Failed to load treasury request", zap.Error(err), zap.String("request_id", requestID)) @@ -190,7 +190,7 @@ func (t *TreasuryRequests) FindActiveByLedgerAccountID(ctx context.Context, ledg err := t.repo.FindOneByFilter(ctx, query, &result) if errors.Is(err, merrors.ErrNoData) { t.logger.Debug("Active treasury request not found", zap.String("ledger_account_id", ledgerAccountID)) - return nil, nil + return nil, nil //nolint:nilnil // nil means no active treasury request for ledger account } if err != nil { t.logger.Warn("Failed to load active treasury request", zap.Error(err), zap.String("ledger_account_id", ledgerAccountID)) diff --git a/api/gateway/chsettle/storage/mongo/store/treasury_telegram_users.go b/api/gateway/chsettle/storage/mongo/store/treasury_telegram_users.go index 271bae10..85b0778a 100644 --- a/api/gateway/chsettle/storage/mongo/store/treasury_telegram_users.go +++ b/api/gateway/chsettle/storage/mongo/store/treasury_telegram_users.go @@ -57,7 +57,7 @@ func (t *TreasuryTelegramUsers) FindByTelegramUserID(ctx context.Context, telegr var result model.TreasuryTelegramUser err := t.repo.FindOneByFilter(ctx, repository.Filter(fieldTreasuryTelegramUserID, telegramUserID), &result) if errors.Is(err, merrors.ErrNoData) { - return nil, nil + return nil, nil //nolint:nilnil // nil means telegram user binding does not exist } if err != nil { if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { @@ -79,7 +79,7 @@ func (t *TreasuryTelegramUsers) FindByTelegramUserID(ctx context.Context, telegr result.AllowedChatIDs = normalized } if result.TelegramUserID == "" || result.LedgerAccountID == "" { - return nil, nil + return nil, nil //nolint:nilnil // incomplete binding is treated as missing user mapping } return &result, nil } diff --git a/api/gateway/common/.golangci.yml b/api/gateway/common/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/gateway/common/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/gateway/mntx/.golangci.yml b/api/gateway/mntx/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/gateway/mntx/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/gateway/mntx/client/client.go b/api/gateway/mntx/client/client.go index 4cbd2e55..5327d0d4 100644 --- a/api/gateway/mntx/client/client.go +++ b/api/gateway/mntx/client/client.go @@ -87,7 +87,7 @@ func (g *gatewayClient) callContext(ctx context.Context, method string) (context } g.logger.Info("Mntx gateway client call timeout applied", fields...) } - return context.WithTimeout(ctx, timeout) + return context.WithTimeout(ctx, timeout) //nolint:gosec // cancel func is always invoked by call sites } func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) { diff --git a/api/gateway/mntx/internal/server/internal/serverimp.go b/api/gateway/mntx/internal/server/internal/serverimp.go index 84498933..84bc9af9 100644 --- a/api/gateway/mntx/internal/server/internal/serverimp.go +++ b/api/gateway/mntx/internal/server/internal/serverimp.go @@ -410,7 +410,7 @@ func buildGatewayLimits(cfg limitsConfig) *gatewayv1.Limits { if bucket == "" { continue } - limits.VelocityLimit[bucket] = int32(value) + limits.VelocityLimit[bucket] = int32(value) //nolint:gosec // velocity limits are validated config values } } @@ -426,7 +426,7 @@ func buildGatewayLimits(cfg limitsConfig) *gatewayv1.Limits { MinAmount: strings.TrimSpace(override.MinAmount), MaxAmount: strings.TrimSpace(override.MaxAmount), MaxFee: strings.TrimSpace(override.MaxFee), - MaxOps: int32(override.MaxOps), + MaxOps: int32(override.MaxOps), //nolint:gosec // max ops is a validated config value } } } @@ -522,11 +522,12 @@ func (i *Imp) startHTTPCallbackServer(svc *mntxservice.Service, cfg callbackRunt }) server := &http.Server{ - Addr: cfg.Address, - Handler: router, + Addr: cfg.Address, + Handler: router, + ReadHeaderTimeout: 5 * time.Second, } - ln, err := net.Listen("tcp", cfg.Address) + ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", cfg.Address) if err != nil { return err } diff --git a/api/gateway/mntx/internal/service/gateway/card_payout_store_test.go b/api/gateway/mntx/internal/service/gateway/card_payout_store_test.go index 3af5a946..39580798 100644 --- a/api/gateway/mntx/internal/service/gateway/card_payout_store_test.go +++ b/api/gateway/mntx/internal/service/gateway/card_payout_store_test.go @@ -53,7 +53,7 @@ func (s *cardPayoutStore) FindByIdempotencyKey(_ context.Context, key string) (* return v, nil } } - return nil, nil + return nil, nil //nolint:nilnil // test store: payout not found by idempotency key } func (s *cardPayoutStore) FindByOperationRef(_ context.Context, ref string) (*model.CardPayout, error) { @@ -64,7 +64,7 @@ func (s *cardPayoutStore) FindByOperationRef(_ context.Context, ref string) (*mo return v, nil } } - return nil, nil + return nil, nil //nolint:nilnil // test store: payout not found by operation ref } func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model.CardPayout, error) { @@ -75,7 +75,7 @@ func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model. return v, nil } } - return nil, nil + return nil, nil //nolint:nilnil // test store: payout not found by payment id } func (s *cardPayoutStore) Upsert(_ context.Context, record *model.CardPayout) error { diff --git a/api/gateway/mntx/internal/service/gateway/card_processor.go b/api/gateway/mntx/internal/service/gateway/card_processor.go index a2262423..49ef63c4 100644 --- a/api/gateway/mntx/internal/service/gateway/card_processor.go +++ b/api/gateway/mntx/internal/service/gateway/card_processor.go @@ -102,7 +102,7 @@ func findOperationRef(operationRef, payoutID string) string { func (p *cardPayoutProcessor) findExistingPayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) { if p == nil || state == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil processor/state means there is no existing payout state to load } if opRef := strings.TrimSpace(state.OperationRef); opRef != "" { existing, err := p.store.Payouts().FindByOperationRef(ctx, opRef) @@ -117,12 +117,12 @@ func (p *cardPayoutProcessor) findExistingPayoutState(ctx context.Context, state } } } - return nil, nil + return nil, nil //nolint:nilnil // nil means no payout state exists for the operation reference } func (p *cardPayoutProcessor) findAndMergePayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) { if p == nil || state == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil processor/state means there is no existing payout state to merge } existing, err := p.findExistingPayoutState(ctx, state) if err != nil { @@ -828,7 +828,7 @@ func (p *cardPayoutProcessor) retryContext() (context.Context, context.CancelFun if timeout <= 0 { return ctx, func() {} } - return context.WithTimeout(ctx, timeout) + return context.WithTimeout(ctx, timeout) //nolint:gosec // cancel func is always invoked by caller } func (p *cardPayoutProcessor) runCardPayoutRetry(req *mntxv1.CardPayoutRequest, attempt uint32, maxAttempts uint32) { @@ -1349,8 +1349,7 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())), ) - cardInput, err := validateCardTokenizeRequest(req, p.config) - if err != nil { + if _, err := validateCardTokenizeRequest(req, p.config); err != nil { p.logger.Warn("Card tokenization validation failed", zap.String("request_id", req.GetRequestId()), zap.String("customer_id", req.GetCustomerId()), @@ -1359,14 +1358,15 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke return nil, err } + // validateCardTokenizeRequest was called above; here we normalize and map fields for provider payload. + req = sanitizeCardTokenizeRequest(req) + cardInput := extractTokenizeCard(req) + projectID, err := p.resolveProjectID(req.GetProjectId(), "request_id", req.GetRequestId()) if err != nil { return nil, err } - req = sanitizeCardTokenizeRequest(req) - cardInput = extractTokenizeCard(req) - client := monetix.NewClient(p.config, p.httpClient, p.logger) apiReq := buildCardTokenizeRequest(projectID, req, cardInput) @@ -1493,7 +1493,8 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt } retryScheduled := false - if state.Status == model.PayoutStatusFailed || state.Status == model.PayoutStatusCancelled || state.Status == model.PayoutStatusNeedsAttention { + switch state.Status { + case model.PayoutStatusFailed, model.PayoutStatusCancelled, model.PayoutStatusNeedsAttention: decision := p.retryPolicy.decideProviderFailure(state.ProviderCode) attemptsUsed := p.currentDispatchAttempt(operationRef) maxAttempts := p.maxDispatchAttempts() @@ -1546,7 +1547,7 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt if !retryScheduled && strings.TrimSpace(state.FailureReason) == "" { state.FailureReason = payoutFailureReason(state.ProviderCode, state.ProviderMessage) } - } else if state.Status == model.PayoutStatusSuccess { + case model.PayoutStatusSuccess: state.FailureReason = "" } diff --git a/api/gateway/mntx/internal/service/gateway/card_processor_test.go b/api/gateway/mntx/internal/service/gateway/card_processor_test.go index d25b0795..52c340a8 100644 --- a/api/gateway/mntx/internal/service/gateway/card_processor_test.go +++ b/api/gateway/mntx/internal/service/gateway/card_processor_test.go @@ -36,6 +36,15 @@ func (s staticClock) Now() time.Time { return s.now } +func mustMarshalJSON(t *testing.T, value any) []byte { + t.Helper() + body, err := json.Marshal(value) + if err != nil { + t.Fatalf("json marshal failed: %v", err) + } + return body +} + func TestCardPayoutProcessor_Submit_Success(t *testing.T) { cfg := monetix.Config{ BaseURL: "https://monetix.test", @@ -57,7 +66,7 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) { Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { resp := monetix.APIResponse{} resp.Operation.RequestID = "req-123" - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), @@ -117,7 +126,7 @@ func TestCardPayoutProcessor_Submit_AcceptedBodyErrorRemainsWaiting(t *testing.T Code: "3062", Message: "Payment details not received", } - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), @@ -309,7 +318,7 @@ func TestCardPayoutProcessor_Submit_SameParentDifferentOperationsStoredSeparatel callN++ resp := monetix.APIResponse{} resp.Operation.RequestID = fmt.Sprintf("req-%d", callN) - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), @@ -397,7 +406,7 @@ func TestCardPayoutProcessor_StrictMode_BlocksSecondOperationUntilFirstFinalCall n := callN.Add(1) resp := monetix.APIResponse{} resp.Operation.RequestID = fmt.Sprintf("req-%d", n) - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), @@ -589,7 +598,7 @@ func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineUntilSuccess(t *t if n == 1 { resp.Code = "10101" resp.Message = "Decline due to amount or frequency limit" - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusTooManyRequests, Body: io.NopCloser(bytes.NewReader(body)), @@ -597,7 +606,7 @@ func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineUntilSuccess(t *t }, nil } resp.Operation.RequestID = "req-retry-success" - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), @@ -658,7 +667,7 @@ func TestCardPayoutProcessor_Submit_ProviderRetryUsesDelayedStrategy(t *testing. Code: "10101", Message: "Decline due to amount or frequency limit", } - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusTooManyRequests, Body: io.NopCloser(bytes.NewReader(body)), @@ -711,7 +720,7 @@ func TestCardPayoutProcessor_Submit_StatusRefreshRetryUsesStatusRefreshStrategy( Code: "3061", Message: "Transaction not found", } - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusBadRequest, Body: io.NopCloser(bytes.NewReader(body)), @@ -810,7 +819,7 @@ func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineThenNeedsAttentio Code: "10101", Message: "Decline due to amount or frequency limit", } - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusTooManyRequests, Body: io.NopCloser(bytes.NewReader(body)), @@ -874,7 +883,7 @@ func TestCardPayoutProcessor_Submit_NonRetryProviderDeclineRemainsFailed(t *test Code: "10003", Message: "Decline by anti-fraud policy", } - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusBadRequest, Body: io.NopCloser(bytes.NewReader(body)), @@ -933,7 +942,7 @@ func TestCardPayoutProcessor_ProcessCallback_RetryableDeclineSchedulesRetry(t *t } else { resp.Operation.RequestID = "req-after-callback-retry" } - body, _ := json.Marshal(resp) + body := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(body)), diff --git a/api/gateway/mntx/internal/service/gateway/card_tokenize_validation_test.go b/api/gateway/mntx/internal/service/gateway/card_tokenize_validation_test.go index aeccf262..e4e17d89 100644 --- a/api/gateway/mntx/internal/service/gateway/card_tokenize_validation_test.go +++ b/api/gateway/mntx/internal/service/gateway/card_tokenize_validation_test.go @@ -40,8 +40,8 @@ func TestValidateCardTokenizeRequest_Expired(t *testing.T) { cfg := testMonetixConfig() req := validCardTokenizeRequest() now := time.Now().UTC() - req.CardExpMonth = uint32(now.Month()) - req.CardExpYear = uint32(now.Year() - 1) + req.CardExpMonth = uint32(now.Month()) //nolint:gosec // month value is bounded by time.Time + req.CardExpYear = uint32(now.Year() - 1) //nolint:gosec // test value intentionally uses previous year _, err := validateCardTokenizeRequest(req, cfg) requireReason(t, err, "expired_card") diff --git a/api/gateway/mntx/internal/service/gateway/connector.go b/api/gateway/mntx/internal/service/gateway/connector.go index 087289f6..2c371a4c 100644 --- a/api/gateway/mntx/internal/service/gateway/connector.go +++ b/api/gateway/mntx/internal/service/gateway/connector.go @@ -251,8 +251,8 @@ func buildCardPayoutRequestFromParams(reader params.Reader, AmountMinor: amountMinor, Currency: currency, CardPan: strings.TrimSpace(reader.String("card_pan")), - CardExpYear: uint32(readerInt64(reader, "card_exp_year")), - CardExpMonth: uint32(readerInt64(reader, "card_exp_month")), + CardExpYear: uint32(readerInt64(reader, "card_exp_year")), //nolint:gosec // values are validated by request validators + CardExpMonth: uint32(readerInt64(reader, "card_exp_month")), //nolint:gosec // values are validated by request validators CardHolder: strings.TrimSpace(reader.String("card_holder")), Metadata: metadataFromReader(reader), OperationRef: operationRef, diff --git a/api/gateway/mntx/internal/service/gateway/payout_execution_mode.go b/api/gateway/mntx/internal/service/gateway/payout_execution_mode.go index c27f9a7e..4c3b3b1d 100644 --- a/api/gateway/mntx/internal/service/gateway/payout_execution_mode.go +++ b/api/gateway/mntx/internal/service/gateway/payout_execution_mode.go @@ -128,12 +128,13 @@ func (m *strictIsolatedPayoutExecutionMode) tryAcquire(operationRef string) (<-c return nil, false, errPayoutExecutionModeStopped } - switch owner := strings.TrimSpace(m.activeOperation); { - case owner == "": + owner := strings.TrimSpace(m.activeOperation) + switch owner { + case "": m.activeOperation = operationRef m.signalLocked() return nil, true, nil - case owner == operationRef: + case operationRef: return nil, true, nil default: return m.waitCh, false, nil diff --git a/api/gateway/mntx/internal/service/gateway/payout_failure_policy_test.go b/api/gateway/mntx/internal/service/gateway/payout_failure_policy_test.go index 1d35914b..931341a3 100644 --- a/api/gateway/mntx/internal/service/gateway/payout_failure_policy_test.go +++ b/api/gateway/mntx/internal/service/gateway/payout_failure_policy_test.go @@ -73,7 +73,6 @@ func TestPayoutFailurePolicy_DecideProviderFailure(t *testing.T) { } for _, tc := range cases { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Helper() got := policy.decideProviderFailure(tc.code) @@ -121,7 +120,6 @@ func TestPayoutFailurePolicy_DocumentRetryCoverage(t *testing.T) { } for _, tc := range cases { - tc := tc t.Run(tc.code, func(t *testing.T) { t.Helper() got := policy.decideProviderFailure(tc.code) @@ -272,7 +270,6 @@ func TestRetryDelayForAttempt_ByStrategy(t *testing.T) { } for _, tc := range cases { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Helper() if got := retryDelayForAttempt(tc.attempt, tc.strategy); got != tc.wantDelay { diff --git a/api/gateway/mntx/internal/service/gateway/service.go b/api/gateway/mntx/internal/service/gateway/service.go index e9eced5f..9bb55c77 100644 --- a/api/gateway/mntx/internal/service/gateway/service.go +++ b/api/gateway/mntx/internal/service/gateway/service.go @@ -174,7 +174,7 @@ func (s *Service) startDiscoveryAnnouncer() { if strings.TrimSpace(announce.ID) == "" { announce.ID = discovery.StablePaymentGatewayID(discovery.RailCardPayout) } - s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.MntxGateway), announce) + s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.MntxGateway, announce) s.announcer.Start() } diff --git a/api/gateway/mntx/internal/service/gateway/testhelpers_test.go b/api/gateway/mntx/internal/service/gateway/testhelpers_test.go index 089ada48..1ad93523 100644 --- a/api/gateway/mntx/internal/service/gateway/testhelpers_test.go +++ b/api/gateway/mntx/internal/service/gateway/testhelpers_test.go @@ -18,8 +18,8 @@ func requireReason(t *testing.T, err error, reason string) { if !errors.Is(err, merrors.ErrInvalidArg) { t.Fatalf("expected invalid argument error, got %v", err) } - reasoned, ok := err.(payoutFailure) - if !ok { + var reasoned payoutFailure + if !errors.As(err, &reasoned) { t.Fatalf("expected payout failure reason, got %T", err) } if reasoned.Reason() != reason { @@ -82,5 +82,5 @@ func validCardTokenizeRequest() *mntxv1.CardTokenizeRequest { func futureExpiry() (uint32, uint32) { now := time.Now().UTC() - return uint32(now.Month()), uint32(now.Year() + 1) + return uint32(now.Month()), uint32(now.Year() + 1) //nolint:gosec // month/year values are bounded by time.Time } diff --git a/api/gateway/mntx/internal/service/gateway/transfer_notifications.go b/api/gateway/mntx/internal/service/gateway/transfer_notifications.go index 5a7ed279..81eb6c33 100644 --- a/api/gateway/mntx/internal/service/gateway/transfer_notifications.go +++ b/api/gateway/mntx/internal/service/gateway/transfer_notifications.go @@ -67,7 +67,7 @@ func (p *cardPayoutProcessor) updatePayoutStatus(ctx context.Context, state *mod return nil, emitErr } } - return nil, nil + return struct{}{}, nil }) if err != nil { p.logger.Warn("Failed to update transfer status", zap.Error(err), mzap.ObjRef("payout_ref", state.ID), diff --git a/api/gateway/mntx/internal/service/monetix/sender.go b/api/gateway/mntx/internal/service/monetix/sender.go index 6b3c6e4a..cd3ba87d 100644 --- a/api/gateway/mntx/internal/service/monetix/sender.go +++ b/api/gateway/mntx/internal/service/monetix/sender.go @@ -129,7 +129,9 @@ func (c *Client) sendTokenization(ctx context.Context, req CardTokenizeRequest) c.logger.Warn("Monetix tokenization request failed", fields...) return nil, merrors.Internal("monetix tokenization request failed: " + err.Error()) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() body, _ := io.ReadAll(resp.Body) outcome := outcomeAccepted @@ -252,7 +254,9 @@ func (c *Client) send(ctx context.Context, req any, path string, dispatchLog fun c.logger.Warn("Monetix request failed", fields...) return nil, merrors.Internal("monetix request failed: " + err.Error()) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/api/gateway/mntx/internal/service/monetix/sender_test.go b/api/gateway/mntx/internal/service/monetix/sender_test.go index b02ebdad..1986568c 100644 --- a/api/gateway/mntx/internal/service/monetix/sender_test.go +++ b/api/gateway/mntx/internal/service/monetix/sender_test.go @@ -21,6 +21,15 @@ func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } +func mustMarshalJSON(t *testing.T, value any) []byte { + t.Helper() + payload, err := json.Marshal(value) + if err != nil { + t.Fatalf("json marshal failed: %v", err) + } + return payload +} + func TestSendCardPayout_SignsPayload(t *testing.T) { secret := "secret" var captured CardPayoutRequest @@ -36,7 +45,7 @@ func TestSendCardPayout_SignsPayload(t *testing.T) { } resp := APIResponse{} resp.Operation.RequestID = "req-1" - payload, _ := json.Marshal(resp) + payload := mustMarshalJSON(t, resp) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(payload)), @@ -94,7 +103,7 @@ func TestSendCardPayout_NormalizesTwoDigitYearBeforeSend(t *testing.T) { if err := json.Unmarshal(body, &captured); err != nil { t.Fatalf("failed to decode request: %v", err) } - payload, _ := json.Marshal(APIResponse{}) + payload := mustMarshalJSON(t, APIResponse{}) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(payload)), @@ -512,7 +521,7 @@ func TestSendCardTokenization_NormalizesTwoDigitYearBeforeSend(t *testing.T) { if err := json.Unmarshal(body, &captured); err != nil { t.Fatalf("failed to decode request: %v", err) } - payload, _ := json.Marshal(APIResponse{}) + payload := mustMarshalJSON(t, APIResponse{}) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(payload)), diff --git a/api/gateway/tgsettle/.golangci.yml b/api/gateway/tgsettle/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/gateway/tgsettle/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/gateway/tgsettle/internal/service/gateway/confirmation_flow.go b/api/gateway/tgsettle/internal/service/gateway/confirmation_flow.go index 21587340..3ebc90a0 100644 --- a/api/gateway/tgsettle/internal/service/gateway/confirmation_flow.go +++ b/api/gateway/tgsettle/internal/service/gateway/confirmation_flow.go @@ -217,7 +217,7 @@ func (s *Service) onTelegramUpdate(ctx context.Context, update *model.TelegramWe ReplyToMessageID: message.MessageID, Text: "Only approved users can confirm this payment.", }); e != nil { - s.logger.Warn("Failed to create telegram text", append(replyFields, zap.Error(err))...) + s.logger.Warn("Failed to create telegram text", append(replyFields, zap.Error(e))...) } if err := s.clearPendingConfirmation(ctx, pending.RequestID); err != nil { return err @@ -242,7 +242,7 @@ func (s *Service) onTelegramUpdate(ctx context.Context, update *model.TelegramWe ReplyToMessageID: message.MessageID, Text: clarificationMessage(reason), }); e != nil { - s.logger.Warn("Failed to create telegram text", append(replyFields, zap.Error(err))...) + s.logger.Warn("Failed to create telegram text", append(replyFields, zap.Error(e))...) } s.logger.Warn("Telegram confirmation reply dropped", append(replyFields, @@ -307,13 +307,13 @@ func (s *Service) publishPendingConfirmationResult(pending *storagemodel.Pending } sourceService := strings.TrimSpace(pending.SourceService) if sourceService == "" { - sourceService = string(mservice.PaymentGateway) + sourceService = mservice.PaymentGateway } rail := strings.TrimSpace(pending.Rail) if rail == "" { rail = s.rail } - env := confirmations.ConfirmationResult(string(mservice.PaymentGateway), result, sourceService, rail) + env := confirmations.ConfirmationResult(mservice.PaymentGateway, result, sourceService, rail) if err := s.producer.SendMessage(env); err != nil { s.logger.Warn("Failed to publish confirmation result", zap.Error(err), zap.String("request_id", strings.TrimSpace(result.RequestID)), @@ -338,7 +338,7 @@ func (s *Service) sendTelegramText(_ context.Context, request *model.TelegramTex if request.ChatID == "" || request.Text == "" { return merrors.InvalidArgument("telegram chat_id and text are required", "chat_id", "text") } - env := tnotifications.TelegramText(string(mservice.PaymentGateway), request) + env := tnotifications.TelegramText(mservice.PaymentGateway, request) if err := s.producer.SendMessage(env); err != nil { s.logger.Warn("Failed to publish telegram text request", zap.Error(err), zap.String("request_id", request.RequestID), diff --git a/api/gateway/tgsettle/internal/service/gateway/connector.go b/api/gateway/tgsettle/internal/service/gateway/connector.go index 8a7d24e4..f0933358 100644 --- a/api/gateway/tgsettle/internal/service/gateway/connector.go +++ b/api/gateway/tgsettle/internal/service/gateway/connector.go @@ -31,7 +31,7 @@ func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilit } func (s *Service) OpenAccount(_ context.Context, _ *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) { - return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported", nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported", nil)}, nil } func (s *Service) GetAccount(_ context.Context, _ *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) { @@ -49,16 +49,16 @@ func (s *Service) GetBalance(_ context.Context, _ *connectorv1.GetBalanceRequest func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) { if req == nil || req.GetOperation() == nil { s.logger.Warn("Submit operation rejected", zap.String("reason", "operation is required")) - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil)}}, nil } op := req.GetOperation() if strings.TrimSpace(op.GetIdempotencyKey()) == "" { s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "idempotency_key is required"))...) - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op)}}, nil } if op.GetType() != connectorv1.OperationType_TRANSFER { s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "unsupported operation type"))...) - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op)}}, nil } reader := params.New(op.GetParams()) metadata := reader.StringMap("metadata") @@ -74,22 +74,22 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp } if paymentIntentID == "" { s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "payment_intent_id is required"))...) - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: payment_intent_id is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: payment_intent_id is required", op)}}, nil } source := operationAccountID(op.GetFrom()) if source == "" { s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "from.account is required"))...) - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: from.account is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: from.account is required", op)}}, nil } dest, err := transferDestinationFromOperation(op) if err != nil { s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.Error(err))...) - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil } amount := op.GetMoney() if amount == nil { s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "money is required"))...) - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op)}}, nil } metadata[metadataPaymentIntentID] = paymentIntentID @@ -133,7 +133,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp }) if err != nil { s.logger.Warn("Submit operation transfer failed", append(logFields, zap.Error(err))...) - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil } transfer := resp.GetTransfer() operationID := strings.TrimSpace(transfer.GetOperationRef()) @@ -142,7 +142,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp zap.String("transfer_ref", strings.TrimSpace(transfer.GetTransferRef())), )...) return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{ - Error: connectorError(connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE, "submit_operation: operation_ref is missing in transfer response", op, ""), + Error: connectorError(connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE, "submit_operation: operation_ref is missing in transfer response", op), }}, nil } s.logger.Info("Submit operation transfer submitted", append(logFields, @@ -361,11 +361,10 @@ func transferDestinationLogFields(dest *chainv1.TransferDestination) []zap.Field } } -func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError { +func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation) *connectorv1.ConnectorError { err := &connectorv1.ConnectorError{ - Code: code, - Message: strings.TrimSpace(message), - AccountId: strings.TrimSpace(accountID), + Code: code, + Message: strings.TrimSpace(message), } if op != nil { err.CorrelationId = strings.TrimSpace(op.GetCorrelationId()) diff --git a/api/gateway/tgsettle/internal/service/gateway/outbox_reliable.go b/api/gateway/tgsettle/internal/service/gateway/outbox_reliable.go index e9e31c6d..ed030a79 100644 --- a/api/gateway/tgsettle/internal/service/gateway/outbox_reliable.go +++ b/api/gateway/tgsettle/internal/service/gateway/outbox_reliable.go @@ -24,15 +24,15 @@ func (s *Service) outboxStore() gatewayoutbox.Store { return provider.Outbox() } -func (s *Service) startOutboxReliableProducer() error { +func (s *Service) startOutboxReliableProducer(_ context.Context) error { if s == nil || s.repo == nil { return nil } - return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg) + return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg) //nolint:contextcheck // Reliable runtime start API does not accept context. } func (s *Service) sendWithOutbox(ctx context.Context, env me.Envelope) error { - if err := s.startOutboxReliableProducer(); err != nil { + if err := s.startOutboxReliableProducer(ctx); err != nil { return err } return s.outbox.Send(ctx, env) diff --git a/api/gateway/tgsettle/internal/service/gateway/service.go b/api/gateway/tgsettle/internal/service/gateway/service.go index fd832ff7..b2d54993 100644 --- a/api/gateway/tgsettle/internal/service/gateway/service.go +++ b/api/gateway/tgsettle/internal/service/gateway/service.go @@ -24,7 +24,6 @@ import ( tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" - pmodel "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" paymenttypes "github.com/tech/sendico/pkg/payments/types" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" @@ -63,7 +62,7 @@ type Config struct { AcceptedUserIDs []string SuccessReaction string InvokeURI string - MessagingSettings pmodel.SettingsT + MessagingSettings model.SettingsT DiscoveryRegistry *discovery.Registry Treasury TreasuryConfig } @@ -90,7 +89,7 @@ type Service struct { producer msg.Producer broker mb.Broker cfg Config - msgCfg pmodel.SettingsT + msgCfg model.SettingsT rail string chatID string announcer *discovery.Announcer @@ -131,7 +130,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro if svc.successReaction == "" { svc.successReaction = defaultTelegramSuccessReaction } - if err := svc.startOutboxReliableProducer(); err != nil { + if err := svc.startOutboxReliableProducer(context.Background()); err != nil { svc.logger.Warn("Failed to initialise outbox reliable producer", zap.Error(err)) } svc.startConsumers() @@ -240,9 +239,9 @@ func (s *Service) startConsumers() { } return } - resultProcessor := confirmations.NewConfirmationResultProcessor(s.logger, string(mservice.PaymentGateway), s.rail, s.onConfirmationResult) + resultProcessor := confirmations.NewConfirmationResultProcessor(s.logger, mservice.PaymentGateway, s.rail, s.onConfirmationResult) s.consumeProcessor(resultProcessor) - dispatchProcessor := confirmations.NewConfirmationDispatchProcessor(s.logger, string(mservice.PaymentGateway), s.rail, s.onConfirmationDispatch) + dispatchProcessor := confirmations.NewConfirmationDispatchProcessor(s.logger, mservice.PaymentGateway, s.rail, s.onConfirmationDispatch) s.consumeProcessor(dispatchProcessor) updateProcessor := tnotifications.NewTelegramUpdateProcessor(s.logger, s.onTelegramUpdate) s.consumeProcessor(updateProcessor) @@ -277,7 +276,7 @@ func (s *Service) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransfe s.logger.Warn("Submit transfer rejected", zap.String("reason", "amount is required"), zap.String("idempotency_key", idempotencyKey)) return nil, merrors.InvalidArgument("submit_transfer: amount is required") } - intent, err := intentFromSubmitTransfer(req, s.rail, s.chatID) + intent, err := intentFromSubmitTransfer(req, s.rail) if err != nil { s.logger.Warn("Submit transfer rejected", zap.Error(err), zap.String("idempotency_key", idempotencyKey)) return nil, err @@ -537,7 +536,7 @@ func (s *Service) buildConfirmationRequest(intent *model.PaymentGatewayIntent) ( QuoteRef: intent.QuoteRef, AcceptedUserIDs: s.cfg.AcceptedUserIDs, TimeoutSeconds: timeout, - SourceService: string(mservice.PaymentGateway), + SourceService: mservice.PaymentGateway, Rail: rail, OperationRef: intent.OperationRef, IntentRef: intent.IntentRef, @@ -554,7 +553,7 @@ func (s *Service) sendConfirmationRequest(request *model.ConfirmationRequest) er s.logger.Warn("Messaging producer not configured") return merrors.Internal("messaging producer is not configured") } - env := confirmations.ConfirmationRequest(string(mservice.PaymentGateway), request) + env := confirmations.ConfirmationRequest(mservice.PaymentGateway, request) if err := s.producer.SendMessage(env); err != nil { s.logger.Warn("Failed to publish confirmation request", zap.Error(err), @@ -590,7 +589,7 @@ func (s *Service) publishTelegramReaction(result *model.ConfirmationResult) { if request.ChatID == "" || request.MessageID == "" || request.Emoji == "" { return } - env := tnotifications.TelegramReaction(string(mservice.PaymentGateway), request) + env := tnotifications.TelegramReaction(mservice.PaymentGateway, request) if err := s.producer.SendMessage(env); err != nil { s.logger.Warn("Failed to publish telegram reaction", zap.Error(err), @@ -632,7 +631,7 @@ func (s *Service) startAnnouncer() { Operations: caps, InvokeURI: s.invokeURI, } - s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.PaymentGateway), announce) + s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.PaymentGateway, announce) s.announcer.Start() } @@ -685,7 +684,7 @@ func paymentRecordFromIntent(intent *model.PaymentGatewayIntent, confirmReq *mod return record } -func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, defaultChatID string) (*model.PaymentGatewayIntent, error) { +func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail string) (*model.PaymentGatewayIntent, error) { if req == nil { return nil, merrors.InvalidArgument("submit_transfer: request is required") } @@ -733,14 +732,10 @@ func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, d return nil, merrors.InvalidArgument("submit_transfer: operation_ref is required") } quoteRef := strings.TrimSpace(metadata[metadataQuoteRef]) - targetChatID := strings.TrimSpace(metadata[metadataTargetChatID]) outgoingLeg := normalizeRail(metadata[metadataOutgoingLeg]) if outgoingLeg == "" { outgoingLeg = normalizeRail(defaultRail) } - if targetChatID == "" { - targetChatID = strings.TrimSpace(defaultChatID) - } return &model.PaymentGatewayIntent{ PaymentRef: paymentRef, PaymentIntentID: paymentIntentID, diff --git a/api/gateway/tgsettle/internal/service/gateway/service_test.go b/api/gateway/tgsettle/internal/service/gateway/service_test.go index bf509dd9..e82407cb 100644 --- a/api/gateway/tgsettle/internal/service/gateway/service_test.go +++ b/api/gateway/tgsettle/internal/service/gateway/service_test.go @@ -32,6 +32,7 @@ func (f *fakePaymentsStore) FindByIdempotencyKey(_ context.Context, key string) f.mu.Lock() defer f.mu.Unlock() if f.records == nil { + //nolint:nilnil // Test stub uses nil, nil to represent missing records. return nil, nil } return f.records[key], nil @@ -41,6 +42,7 @@ func (f *fakePaymentsStore) FindByOperationRef(_ context.Context, key string) (* f.mu.Lock() defer f.mu.Unlock() if f.records == nil { + //nolint:nilnil // Test stub uses nil, nil to represent missing records. return nil, nil } for _, record := range f.records { @@ -48,6 +50,7 @@ func (f *fakePaymentsStore) FindByOperationRef(_ context.Context, key string) (* return record, nil } } + //nolint:nilnil // Test stub uses nil, nil to represent missing records. return nil, nil } @@ -124,6 +127,7 @@ func (f *fakePendingStore) FindByRequestID(_ context.Context, requestID string) f.mu.Lock() defer f.mu.Unlock() if f.records == nil { + //nolint:nilnil // Test stub uses nil, nil to represent missing records. return nil, nil } return f.records[requestID], nil @@ -137,6 +141,7 @@ func (f *fakePendingStore) FindByMessageID(_ context.Context, messageID string) return record, nil } } + //nolint:nilnil // Test stub uses nil, nil to represent missing records. return nil, nil } @@ -197,6 +202,7 @@ func (f *fakeBroker) Publish(_ envelope.Envelope) error { } func (f *fakeBroker) Subscribe(event model.NotificationEvent) (<-chan envelope.Envelope, error) { + //nolint:nilnil // Test stub does not create a subscription channel. return nil, nil } @@ -237,7 +243,7 @@ func newTestService(_ *testing.T) (*Service, *fakeRepo, *captureProducer) { pending: &fakePendingStore{}, } - sigEnv := tnotifications.TelegramReaction(string(mservice.PaymentGateway), &model.TelegramReactionRequest{ + sigEnv := tnotifications.TelegramReaction(mservice.PaymentGateway, &model.TelegramReactionRequest{ RequestID: "x", ChatID: "1", MessageID: "2", @@ -407,7 +413,7 @@ func TestIntentFromSubmitTransfer_NormalizesOutgoingLeg(t *testing.T) { Metadata: map[string]string{ metadataOutgoingLeg: "card", }, - }, "provider_settlement", "") + }, "provider_settlement") if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/api/gateway/tgsettle/internal/service/gateway/transfer_notifications.go b/api/gateway/tgsettle/internal/service/gateway/transfer_notifications.go index b926bf16..7b2c1923 100644 --- a/api/gateway/tgsettle/internal/service/gateway/transfer_notifications.go +++ b/api/gateway/tgsettle/internal/service/gateway/transfer_notifications.go @@ -53,7 +53,7 @@ func (s *Service) updateTransferStatus(ctx context.Context, record *model.Paymen return nil, emitErr } } - return nil, nil + return struct{}{}, nil }) if err != nil { s.logger.Warn("Failed to update transfer status", zap.String("payment_ref", record.PaymentIntentID), zap.String("status", string(record.Status)), zap.Error(err)) diff --git a/api/gateway/tgsettle/internal/service/treasury/bot/router.go b/api/gateway/tgsettle/internal/service/treasury/bot/router.go index 728af287..dc5a2b20 100644 --- a/api/gateway/tgsettle/internal/service/treasury/bot/router.go +++ b/api/gateway/tgsettle/internal/service/treasury/bot/router.go @@ -22,7 +22,7 @@ const amountInputHint = "*Amount format*\nEnter amount as a decimal number using type SendTextFunc func(ctx context.Context, chatID string, text string) error type ScheduleTracker interface { - TrackScheduled(record *storagemodel.TreasuryRequest) + TrackScheduled(ctx context.Context, record *storagemodel.TreasuryRequest) Untrack(requestID string) } @@ -128,6 +128,11 @@ func (r *Router) HandleUpdate(ctx context.Context, update *model.TelegramWebhook binding, err := r.users.ResolveUserBinding(ctx, userID) if err != nil { + if errors.Is(err, merrors.ErrNoData) { + r.logUnauthorized(update) + _ = r.sendText(ctx, chatID, unauthorizedMessage) + return true + } if r.logger != nil { r.logger.Warn("Failed to resolve treasury user binding", zap.Error(err), @@ -224,7 +229,7 @@ func (r *Router) HandleUpdate(ctx context.Context, update *model.TelegramWebhook func (r *Router) startAmountDialog(ctx context.Context, userID, accountID, chatID string, operation storagemodel.TreasuryOperationType) { active, err := r.service.GetActiveRequestForAccount(ctx, accountID) - if err != nil { + if err != nil && !errors.Is(err, merrors.ErrNoData) { if r.logger != nil { r.logger.Warn("Failed to check active treasury request", zap.Error(err), zap.String("telegram_user_id", userID), zap.String("ledger_account_id", accountID)) } @@ -267,7 +272,8 @@ func (r *Router) captureAmount(ctx context.Context, userID, accountID, chatID st }) return } - if typed, ok := err.(limitError); ok { + var typed limitError + if errors.As(err, &typed) { switch typed.LimitKind() { case "per_operation": _ = r.sendText(ctx, chatID, "*Amount exceeds allowed limit*\n\n*Max per operation:* "+markdownCode(typed.LimitMax())+"\n\nEnter another amount or "+markdownCommand(CommandCancel)+".") @@ -316,7 +322,7 @@ func (r *Router) confirm(ctx context.Context, userID string, accountID string, c return } if r.tracker != nil { - r.tracker.TrackScheduled(record) + r.tracker.TrackScheduled(ctx, record) } r.dialogs.Clear(userID) delay := int64(r.service.ExecutionDelay().Seconds()) @@ -487,7 +493,7 @@ func (r *Router) resolveAccountProfile(ctx context.Context, ledgerAccountID stri } profile, err := r.service.GetAccountProfile(ctx, ledgerAccountID) if err != nil { - if r.logger != nil { + if r.logger != nil && !errors.Is(err, merrors.ErrNoData) { r.logger.Warn("Failed to resolve treasury account profile", zap.Error(err), zap.String("ledger_account_id", strings.TrimSpace(ledgerAccountID))) diff --git a/api/gateway/tgsettle/internal/service/treasury/bot/router_test.go b/api/gateway/tgsettle/internal/service/treasury/bot/router_test.go index b4b4bfb3..f9c5bc45 100644 --- a/api/gateway/tgsettle/internal/service/treasury/bot/router_test.go +++ b/api/gateway/tgsettle/internal/service/treasury/bot/router_test.go @@ -22,6 +22,7 @@ func (f fakeUserBindingResolver) ResolveUserBinding(_ context.Context, telegramU return nil, f.err } if f.bindings == nil { + //nolint:nilnil // Test stub uses nil, nil to represent missing binding. return nil, nil } return f.bindings[telegramUserID], nil @@ -36,6 +37,7 @@ func (fakeService) MaxPerOperationLimit() string { } func (fakeService) GetActiveRequestForAccount(context.Context, string) (*storagemodel.TreasuryRequest, error) { + //nolint:nilnil // Test stub uses nil, nil to represent no active request. return nil, nil } @@ -48,14 +50,17 @@ func (fakeService) GetAccountProfile(_ context.Context, ledgerAccountID string) } func (fakeService) CreateRequest(context.Context, CreateRequestInput) (*storagemodel.TreasuryRequest, error) { + //nolint:nilnil // Test stub uses nil, nil to represent no created request. return nil, nil } func (fakeService) ConfirmRequest(context.Context, string, string) (*storagemodel.TreasuryRequest, error) { + //nolint:nilnil // Test stub uses nil, nil to represent no confirmed request. return nil, nil } func (fakeService) CancelRequest(context.Context, string, string) (*storagemodel.TreasuryRequest, error) { + //nolint:nilnil // Test stub uses nil, nil to represent no cancelled request. return nil, nil } diff --git a/api/gateway/tgsettle/internal/service/treasury/ledger/client.go b/api/gateway/tgsettle/internal/service/treasury/ledger/client.go index 3fa70002..43144c1b 100644 --- a/api/gateway/tgsettle/internal/service/treasury/ledger/client.go +++ b/api/gateway/tgsettle/internal/service/treasury/ledger/client.go @@ -260,7 +260,7 @@ func (c *connectorClient) callContext(ctx context.Context) (context.Context, con if ctx == nil { ctx = context.Background() } - return context.WithTimeout(ctx, c.cfg.Timeout) + return context.WithTimeout(ctx, c.cfg.Timeout) //nolint:gosec // cancel func is always invoked by call sites } func structFromMap(values map[string]any) *structpb.Struct { diff --git a/api/gateway/tgsettle/internal/service/treasury/ledger/discovery_client.go b/api/gateway/tgsettle/internal/service/treasury/ledger/discovery_client.go index 1bee6a1d..d6b40213 100644 --- a/api/gateway/tgsettle/internal/service/treasury/ledger/discovery_client.go +++ b/api/gateway/tgsettle/internal/service/treasury/ledger/discovery_client.go @@ -141,7 +141,7 @@ func (c *discoveryClient) resolveClient(_ context.Context) (Client, error) { c.endpointKey = key if c.logger != nil { c.logger.Info("Discovered ledger endpoint selected", - zap.String("service", string(mservice.Ledger)), + zap.String("service", mservice.Ledger), zap.String("invoke_uri", endpoint.raw), zap.String("address", endpoint.address), zap.Bool("insecure", endpoint.insecure)) @@ -186,10 +186,10 @@ func (c *discoveryClient) resolveEndpoint() (discoveryEndpoint, error) { func matchesService(service string, candidate mservice.Type) bool { service = strings.TrimSpace(service) - if service == "" || strings.TrimSpace(string(candidate)) == "" { + if service == "" || strings.TrimSpace(candidate) == "" { return false } - return strings.EqualFold(service, strings.TrimSpace(string(candidate))) + return strings.EqualFold(service, strings.TrimSpace(candidate)) } func parseDiscoveryEndpoint(raw string) (discoveryEndpoint, error) { diff --git a/api/gateway/tgsettle/internal/service/treasury/module.go b/api/gateway/tgsettle/internal/service/treasury/module.go index d5cfdd5a..78d80ae9 100644 --- a/api/gateway/tgsettle/internal/service/treasury/module.go +++ b/api/gateway/tgsettle/internal/service/treasury/module.go @@ -106,7 +106,7 @@ func (a *botUsersAdapter) ResolveUserBinding(ctx context.Context, telegramUserID return nil, err } if record == nil { - return nil, nil + return nil, merrors.NoData("treasury user binding not found") } return &bot.UserBinding{ TelegramUserID: strings.TrimSpace(record.TelegramUserID), @@ -145,7 +145,7 @@ func (a *botServiceAdapter) GetAccountProfile(ctx context.Context, ledgerAccount return nil, err } if profile == nil { - return nil, nil + return nil, merrors.NoData("treasury account profile not found") } return &bot.AccountProfile{ AccountID: strings.TrimSpace(profile.AccountID), diff --git a/api/gateway/tgsettle/internal/service/treasury/scheduler.go b/api/gateway/tgsettle/internal/service/treasury/scheduler.go index 30cd4177..e827389e 100644 --- a/api/gateway/tgsettle/internal/service/treasury/scheduler.go +++ b/api/gateway/tgsettle/internal/service/treasury/scheduler.go @@ -2,11 +2,13 @@ package treasury import ( "context" + "errors" "strings" "sync" "time" storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model" + "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "go.uber.org/zap" ) @@ -86,7 +88,7 @@ func (s *Scheduler) Shutdown() { s.timersMu.Unlock() } -func (s *Scheduler) TrackScheduled(record *storagemodel.TreasuryRequest) { +func (s *Scheduler) TrackScheduled(ctx context.Context, record *storagemodel.TreasuryRequest) { if s == nil || s.service == nil || record == nil { return } @@ -101,10 +103,14 @@ func (s *Scheduler) TrackScheduled(record *storagemodel.TreasuryRequest) { if when.IsZero() { when = time.Now() } + runCtx := ctx + if runCtx == nil { + runCtx = context.Background() + } delay := time.Until(when) if delay <= 0 { s.Untrack(requestID) - go s.executeAndNotifyByID(context.Background(), requestID) + go s.executeAndNotifyByID(runCtx, requestID) //nolint:gosec // scheduler intentionally dispatches due execution asynchronously return } @@ -114,7 +120,7 @@ func (s *Scheduler) TrackScheduled(record *storagemodel.TreasuryRequest) { } s.timers[requestID] = time.AfterFunc(delay, func() { s.Untrack(requestID) - s.executeAndNotifyByID(context.Background(), requestID) + s.executeAndNotifyByID(runCtx, requestID) }) s.timersMu.Unlock() } @@ -145,7 +151,7 @@ func (s *Scheduler) hydrateTimers(ctx context.Context) { return } for _, record := range scheduled { - s.TrackScheduled(&record) + s.TrackScheduled(ctx, &record) } } @@ -187,16 +193,20 @@ func (s *Scheduler) executeAndNotifyByID(ctx context.Context, requestID string) if requestID == "" { return } - runCtx := ctx if runCtx == nil { runCtx = context.Background() } + withTimeout, cancel := context.WithTimeout(runCtx, 30*time.Second) defer cancel() result, err := s.service.ExecuteRequest(withTimeout, requestID) if err != nil { + if errors.Is(err, merrors.ErrNoData) { + s.logger.Debug("Treasury request is not due for execution", zap.String("request_id", requestID)) + return + } s.logger.Warn("Failed to execute treasury request", zap.Error(err), zap.String("request_id", requestID)) return } @@ -228,11 +238,7 @@ func (s *Scheduler) executeAndNotifyByID(ctx context.Context, requestID string) zap.String("chat_id", chatID), zap.String("status", strings.TrimSpace(string(result.Request.Status)))) - notifyCtx := context.Background() - if ctx != nil { - notifyCtx = ctx - } - notifyCtx, notifyCancel := context.WithTimeout(notifyCtx, 15*time.Second) + notifyCtx, notifyCancel := context.WithTimeout(runCtx, 15*time.Second) defer notifyCancel() if err := s.notify(notifyCtx, chatID, text); err != nil { diff --git a/api/gateway/tgsettle/internal/service/treasury/service.go b/api/gateway/tgsettle/internal/service/treasury/service.go index 03c71b09..d09470dc 100644 --- a/api/gateway/tgsettle/internal/service/treasury/service.go +++ b/api/gateway/tgsettle/internal/service/treasury/service.go @@ -298,33 +298,33 @@ func (s *Service) ExecuteRequest(ctx context.Context, requestID string) (*Execut return nil, err } if record == nil { - return nil, nil + return nil, merrors.NoData("treasury request not found") } switch record.Status { case storagemodel.TreasuryRequestStatusExecuted, storagemodel.TreasuryRequestStatusCancelled, storagemodel.TreasuryRequestStatusFailed: - return nil, nil + return nil, merrors.NoData("treasury request is not executable") case storagemodel.TreasuryRequestStatusScheduled: claimed, err := s.repo.ClaimScheduled(ctx, requestID) if err != nil { return nil, err } if !claimed { - return nil, nil + return nil, merrors.NoData("treasury request is already claimed") } record, err = s.repo.FindByRequestID(ctx, requestID) if err != nil { return nil, err } if record == nil { - return nil, nil + return nil, merrors.NoData("treasury request not found") } } if record.Status != storagemodel.TreasuryRequestStatusConfirmed { - return nil, nil + return nil, merrors.NoData("treasury request is not confirmed") } return s.executeClaimed(ctx, record) } diff --git a/api/gateway/tgsettle/storage/mongo/store/payments.go b/api/gateway/tgsettle/storage/mongo/store/payments.go index 3a8f83e6..3f2e9d24 100644 --- a/api/gateway/tgsettle/storage/mongo/store/payments.go +++ b/api/gateway/tgsettle/storage/mongo/store/payments.go @@ -68,6 +68,7 @@ func (p *Payments) FindByIdempotencyKey(ctx context.Context, key string) (*model var result model.PaymentRecord err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldIdempotencyKey, key), &result) if errors.Is(err, merrors.ErrNoData) { + //nolint:nilnil // Not-found is represented as (nil, nil) by this store contract. return nil, nil } if err != nil { @@ -87,6 +88,7 @@ func (p *Payments) FindByOperationRef(ctx context.Context, key string) (*model.P var result model.PaymentRecord err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldOperationRef, key), &result) if errors.Is(err, merrors.ErrNoData) { + //nolint:nilnil // Not-found is represented as (nil, nil) by this store contract. return nil, nil } if err != nil { diff --git a/api/gateway/tgsettle/storage/mongo/store/pending_confirmations.go b/api/gateway/tgsettle/storage/mongo/store/pending_confirmations.go index b663fef3..7128f58a 100644 --- a/api/gateway/tgsettle/storage/mongo/store/pending_confirmations.go +++ b/api/gateway/tgsettle/storage/mongo/store/pending_confirmations.go @@ -114,6 +114,7 @@ func (p *PendingConfirmations) FindByRequestID(ctx context.Context, requestID st var result model.PendingConfirmation err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldPendingRequestID, requestID), &result) if errors.Is(err, merrors.ErrNoData) { + //nolint:nilnil // Not-found is represented as (nil, nil) by this store contract. return nil, nil } if err != nil { @@ -130,6 +131,7 @@ func (p *PendingConfirmations) FindByMessageID(ctx context.Context, messageID st var result model.PendingConfirmation err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldPendingMessageID, messageID), &result) if errors.Is(err, merrors.ErrNoData) { + //nolint:nilnil // Not-found is represented as (nil, nil) by this store contract. return nil, nil } if err != nil { diff --git a/api/gateway/tgsettle/storage/mongo/store/treasury_requests.go b/api/gateway/tgsettle/storage/mongo/store/treasury_requests.go index 28dfe29c..614e456a 100644 --- a/api/gateway/tgsettle/storage/mongo/store/treasury_requests.go +++ b/api/gateway/tgsettle/storage/mongo/store/treasury_requests.go @@ -165,6 +165,7 @@ func (t *TreasuryRequests) FindByRequestID(ctx context.Context, requestID string err := t.repo.FindOneByFilter(ctx, repository.Filter(fieldTreasuryRequestID, requestID), &result) if errors.Is(err, merrors.ErrNoData) { t.logger.Debug("Treasury request not found", zap.String("request_id", requestID)) + //nolint:nilnil // Not-found is represented as (nil, nil) by this store contract. return nil, nil } if err != nil { @@ -190,6 +191,7 @@ func (t *TreasuryRequests) FindActiveByLedgerAccountID(ctx context.Context, ledg err := t.repo.FindOneByFilter(ctx, query, &result) if errors.Is(err, merrors.ErrNoData) { t.logger.Debug("Active treasury request not found", zap.String("ledger_account_id", ledgerAccountID)) + //nolint:nilnil // Not-found is represented as (nil, nil) by this store contract. return nil, nil } if err != nil { diff --git a/api/gateway/tgsettle/storage/mongo/store/treasury_telegram_users.go b/api/gateway/tgsettle/storage/mongo/store/treasury_telegram_users.go index 04c4e597..508a0752 100644 --- a/api/gateway/tgsettle/storage/mongo/store/treasury_telegram_users.go +++ b/api/gateway/tgsettle/storage/mongo/store/treasury_telegram_users.go @@ -57,6 +57,7 @@ func (t *TreasuryTelegramUsers) FindByTelegramUserID(ctx context.Context, telegr var result model.TreasuryTelegramUser err := t.repo.FindOneByFilter(ctx, repository.Filter(fieldTreasuryTelegramUserID, telegramUserID), &result) if errors.Is(err, merrors.ErrNoData) { + //nolint:nilnil // Not-found is represented as (nil, nil) by this store contract. return nil, nil } if err != nil { @@ -79,6 +80,7 @@ func (t *TreasuryTelegramUsers) FindByTelegramUserID(ctx context.Context, telegr result.AllowedChatIDs = normalized } if result.TelegramUserID == "" || result.LedgerAccountID == "" { + //nolint:nilnil // Invalid/incomplete records are treated as missing binding. return nil, nil } return &result, nil diff --git a/api/gateway/tron/.golangci.yml b/api/gateway/tron/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/gateway/tron/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/gateway/tron/client/client.go b/api/gateway/tron/client/client.go index a72831d7..1e29d4cf 100644 --- a/api/gateway/tron/client/client.go +++ b/api/gateway/tron/client/client.go @@ -321,11 +321,13 @@ func (c *chainGatewayClient) callContext(ctx context.Context) (context.Context, if timeout <= 0 { timeout = 3 * time.Second } + //nolint:gosec // Caller receives cancel func and defers it in every call path. return context.WithTimeout(ctx, timeout) } func walletParamsFromRequest(req *chainv1.CreateManagedWalletRequest) (*structpb.Struct, error) { if req == nil { + //nolint:nilnil // Nil request means optional params are absent. return nil, nil } params := map[string]interface{}{ diff --git a/api/gateway/tron/internal/keymanager/vault/manager.go b/api/gateway/tron/internal/keymanager/vault/manager.go index 105e2e17..a2223b04 100644 --- a/api/gateway/tron/internal/keymanager/vault/manager.go +++ b/api/gateway/tron/internal/keymanager/vault/manager.go @@ -36,7 +36,7 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) { } keys, err := managedkey.New(managedkey.Options{ Logger: logger, - Config: managedkey.Config(cfg), + Config: cfg, Component: "vault key manager", DefaultKeyPrefix: "gateway/tron/wallets", }) diff --git a/api/gateway/tron/internal/server/internal/serverimp.go b/api/gateway/tron/internal/server/internal/serverimp.go index 6dc55905..fab5b157 100644 --- a/api/gateway/tron/internal/server/internal/serverimp.go +++ b/api/gateway/tron/internal/server/internal/serverimp.go @@ -318,6 +318,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew func buildGasTopUpPolicy(chainName string, cfg *gasTopUpPolicyConfig) (*gatewayshared.GasTopUpPolicy, error) { if cfg == nil { + //nolint:nilnil // Nil config means gas top-up policy is intentionally disabled. return nil, nil } defaultRule, defaultSet, err := parseGasTopUpRule(chainName, "default", cfg.gasTopUpRuleConfig) diff --git a/api/gateway/tron/internal/service/gateway/commands/transfer/gas_topup.go b/api/gateway/tron/internal/service/gateway/commands/transfer/gas_topup.go index 0f0f661b..f1a17386 100644 --- a/api/gateway/tron/internal/service/gateway/commands/transfer/gas_topup.go +++ b/api/gateway/tron/internal/service/gateway/commands/transfer/gas_topup.go @@ -235,6 +235,7 @@ func defaultGasTopUp(estimatedFee *moneyv1.Money, currentBalance *moneyv1.Money) } required := estimated.Sub(current) if !required.IsPositive() { + //nolint:nilnil // No top-up required is represented as (nil, nil). return nil, nil } return &moneyv1.Money{ diff --git a/api/gateway/tron/internal/service/gateway/commands/wallet/create.go b/api/gateway/tron/internal/service/gateway/commands/wallet/create.go index 10297d41..cbd2b2a8 100644 --- a/api/gateway/tron/internal/service/gateway/commands/wallet/create.go +++ b/api/gateway/tron/internal/service/gateway/commands/wallet/create.go @@ -147,7 +147,7 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C Metadata: metadata, } if description != nil { - wallet.Describable.Description = description + wallet.Description = description } created, err := c.deps.Storage.Wallets().Create(ctx, wallet) diff --git a/api/gateway/tron/internal/service/gateway/connector.go b/api/gateway/tron/internal/service/gateway/connector.go index 899f7505..eed7d8a4 100644 --- a/api/gateway/tron/internal/service/gateway/connector.go +++ b/api/gateway/tron/internal/service/gateway/connector.go @@ -42,19 +42,19 @@ func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilit func (s *Service) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) { if req == nil { - return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil)}, nil } if req.GetKind() != connectorv1.AccountKind_CHAIN_MANAGED_WALLET { - return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil)}, nil } reader := params.New(req.GetParams()) orgRef := strings.TrimSpace(reader.String("organization_ref")) if orgRef == "" { - return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref is required", nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref is required", nil)}, nil } asset, err := parseChainAsset(strings.TrimSpace(req.GetAsset()), reader) if err != nil { - return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil)}, nil } resp, err := s.CreateManagedWallet(ctx, &chainv1.CreateManagedWalletRequest{ @@ -66,7 +66,7 @@ func (s *Service) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountR Describable: describableFromLabel(req.GetLabel(), reader.String("description")), }) if err != nil { - return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil + return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil)}, nil } return &connectorv1.OpenAccountResponse{Account: chainWalletToAccount(resp.GetWallet())}, nil } @@ -136,32 +136,32 @@ func (s *Service) GetBalance(ctx context.Context, req *connectorv1.GetBalanceReq func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) { if req == nil || req.GetOperation() == nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil)}}, nil } op := req.GetOperation() if strings.TrimSpace(op.GetIdempotencyKey()) == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op)}}, nil } reader := params.New(op.GetParams()) orgRef := strings.TrimSpace(reader.String("organization_ref")) source := operationAccountID(op.GetFrom()) if source == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "operation: from.account is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "operation: from.account is required", op)}}, nil } switch op.GetType() { case connectorv1.OperationType_TRANSFER: dest, err := transferDestinationFromOperation(op) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil } amount := op.GetMoney() if amount == nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op)}}, nil } amount = normalizeMoneyForChain(amount) if orgRef == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: organization_ref is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: organization_ref is required", op)}}, nil } resp, err := s.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{ IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()), @@ -176,7 +176,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp OperationRef: strings.TrimSpace(op.GetOperationRef()), }) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil } transfer := resp.GetTransfer() return &connectorv1.SubmitOperationResponse{ @@ -189,11 +189,11 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp case connectorv1.OperationType_FEE_ESTIMATE: dest, err := transferDestinationFromOperation(op) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil } amount := op.GetMoney() if amount == nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "estimate: money is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "estimate: money is required", op)}}, nil } amount = normalizeMoneyForChain(amount) opID := strings.TrimSpace(op.GetOperationId()) @@ -206,7 +206,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp Amount: amount, }) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil } result := feeEstimateResult(resp) return &connectorv1.SubmitOperationResponse{ @@ -219,7 +219,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp case connectorv1.OperationType_GAS_TOPUP: fee, err := parseMoneyFromMap(reader.Map("estimated_total_fee")) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil } fee = normalizeMoneyForChain(fee) mode := strings.ToLower(strings.TrimSpace(reader.String("mode"))) @@ -237,7 +237,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp EstimatedTotalFee: fee, }) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil } return &connectorv1.SubmitOperationResponse{ Receipt: &connectorv1.OperationReceipt{ @@ -252,11 +252,11 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp opID = strings.TrimSpace(op.GetIdempotencyKey()) } if orgRef == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: organization_ref is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: organization_ref is required", op)}}, nil } target := strings.TrimSpace(reader.String("target_wallet_ref")) if target == "" { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: target_wallet_ref is required", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: target_wallet_ref is required", op)}}, nil } resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{ IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()), @@ -270,7 +270,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp PaymentRef: strings.TrimSpace(reader.String("payment_ref")), }) if err != nil { - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil } transferRef := "" if transfer := resp.GetTransfer(); transfer != nil { @@ -284,10 +284,10 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp }, }, nil default: - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: invalid mode", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "gas_topup: invalid mode", op)}}, nil } default: - return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil + return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op)}}, nil } } @@ -722,11 +722,10 @@ func structFromMap(values map[string]interface{}) *structpb.Struct { return result } -func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError { +func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation) *connectorv1.ConnectorError { err := &connectorv1.ConnectorError{ - Code: code, - Message: strings.TrimSpace(message), - AccountId: strings.TrimSpace(accountID), + Code: code, + Message: strings.TrimSpace(message), } if op != nil { err.CorrelationId = strings.TrimSpace(op.GetCorrelationId()) diff --git a/api/gateway/tron/internal/service/gateway/driver/tron/confirmation.go b/api/gateway/tron/internal/service/gateway/driver/tron/confirmation.go index 9e4497d9..89485786 100644 --- a/api/gateway/tron/internal/service/gateway/driver/tron/confirmation.go +++ b/api/gateway/tron/internal/service/gateway/driver/tron/confirmation.go @@ -90,11 +90,15 @@ func convertTronInfoToReceipt(txInfo *troncore.TransactionInfo) *types.Receipt { } // Create a receipt that's compatible with the existing codebase + gasUsed := uint64(0) + if txInfo.Fee > 0 { + gasUsed = uint64(txInfo.Fee) + } receipt := &types.Receipt{ Status: status, BlockNumber: big.NewInt(txInfo.BlockNumber), // TRON fees are in SUN, but we store as-is for logging - GasUsed: uint64(txInfo.Fee), + GasUsed: gasUsed, } // Set transaction hash from txInfo.Id @@ -141,8 +145,7 @@ func GetTransactionStatus( txInfo, err := tronClient.GetTransactionInfoByID(normalizedHash) if err != nil { - // Transaction not found or error - treat as pending - return -1, nil + return -1, err } if txInfo == nil || txInfo.BlockNumber == 0 { diff --git a/api/gateway/tron/internal/service/gateway/driver/tron/transfer.go b/api/gateway/tron/internal/service/gateway/driver/tron/transfer.go index 78a8da85..cda78cc3 100644 --- a/api/gateway/tron/internal/service/gateway/driver/tron/transfer.go +++ b/api/gateway/tron/internal/service/gateway/driver/tron/transfer.go @@ -279,8 +279,14 @@ func toBaseUnits(amountStr string, decimals *big.Int) (*big.Int, error) { return nil, merrors.InvalidArgument("tron driver: amount must be non-negative") } - shift := int32(decimals.Int64()) - multiplier := decimal.NewFromInt(1).Shift(shift) + decimalsValue := decimals.Int64() + if decimalsValue > 255 { + return nil, merrors.InvalidArgument("tron driver: token decimals out of range") + } + multiplier := decimal.NewFromInt(1) + for i := int64(0); i < decimalsValue; i++ { + multiplier = multiplier.Mul(decimal.NewFromInt(10)) + } scaled := value.Mul(multiplier) if !scaled.Equal(scaled.Truncate(0)) { return nil, merrors.InvalidArgument("tron driver: amount " + trimmed + " exceeds token precision") diff --git a/api/gateway/tron/internal/service/gateway/outbox_reliable.go b/api/gateway/tron/internal/service/gateway/outbox_reliable.go index a7b459e2..e2944a08 100644 --- a/api/gateway/tron/internal/service/gateway/outbox_reliable.go +++ b/api/gateway/tron/internal/service/gateway/outbox_reliable.go @@ -24,15 +24,15 @@ func (s *Service) outboxStore() gatewayoutbox.Store { return provider.Outbox() } -func (s *Service) startOutboxReliableProducer() error { +func (s *Service) startOutboxReliableProducer(_ context.Context) error { if s == nil || s.storage == nil { return nil } - return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg) + return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg) //nolint:contextcheck // Reliable runtime start API does not accept context. } func (s *Service) sendWithOutbox(ctx context.Context, env me.Envelope) error { - if err := s.startOutboxReliableProducer(); err != nil { + if err := s.startOutboxReliableProducer(ctx); err != nil { return err } return s.outbox.Send(ctx, env) diff --git a/api/gateway/tron/internal/service/gateway/rpcclient/clients.go b/api/gateway/tron/internal/service/gateway/rpcclient/clients.go index 356a452f..6f8e208c 100644 --- a/api/gateway/tron/internal/service/gateway/rpcclient/clients.go +++ b/api/gateway/tron/internal/service/gateway/rpcclient/clients.go @@ -169,7 +169,9 @@ func (l *loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro } bodyBytes, _ := io.ReadAll(resp.Body) - resp.Body.Close() + if closeErr := resp.Body.Close(); closeErr != nil { + l.logger.Warn("Failed to close RPC response body", append(fields, zap.Error(closeErr))...) + } resp.Body = io.NopCloser(strings.NewReader(string(bodyBytes))) respFields := append(fields, diff --git a/api/gateway/tron/internal/service/gateway/service.go b/api/gateway/tron/internal/service/gateway/service.go index 89b1de9c..1dc5b0bb 100644 --- a/api/gateway/tron/internal/service/gateway/service.go +++ b/api/gateway/tron/internal/service/gateway/service.go @@ -91,7 +91,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro } svc.settings = svc.settings.withDefaults() svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients) - if err := svc.startOutboxReliableProducer(); err != nil { + if err := svc.startOutboxReliableProducer(context.Background()); err != nil { svc.logger.Warn("Failed to initialise outbox reliable producer", zap.Error(err)) } @@ -236,7 +236,7 @@ func (s *Service) startDiscoveryAnnouncers() { InvokeURI: s.invokeURI, Version: version, } - announcer := discovery.NewAnnouncer(s.logger, s.producer, string(mservice.ChainGateway), announce) + announcer := discovery.NewAnnouncer(s.logger, s.producer, mservice.ChainGateway, announce) announcer.Start() s.announcers = append(s.announcers, announcer) } diff --git a/api/gateway/tron/internal/service/gateway/service_test.go b/api/gateway/tron/internal/service/gateway/service_test.go index 9fedaf50..a4ef4e3a 100644 --- a/api/gateway/tron/internal/service/gateway/service_test.go +++ b/api/gateway/tron/internal/service/gateway/service_test.go @@ -326,7 +326,7 @@ type walletsNoDataRepository struct { } func (r *walletsNoDataRepository) Wallets() storage.WalletsStore { - return &walletsNoDataStore{WalletsStore: r.inMemoryRepository.wallets} + return &walletsNoDataStore{WalletsStore: r.wallets} } type walletsNoDataStore struct { @@ -727,10 +727,11 @@ func sanitizeLimit(requested int32, def, max int64) int64 { if requested <= 0 { return def } - if requested > int32(max) { + requested64 := int64(requested) + if requested64 > max { return max } - return int64(requested) + return requested64 } func newTestService(t *testing.T) (*Service, *inMemoryRepository) { diff --git a/api/gateway/tron/internal/service/gateway/transfer_execution.go b/api/gateway/tron/internal/service/gateway/transfer_execution.go index 39d38f55..7a51f369 100644 --- a/api/gateway/tron/internal/service/gateway/transfer_execution.go +++ b/api/gateway/tron/internal/service/gateway/transfer_execution.go @@ -40,26 +40,26 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet return err } - if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusProcessing, "", ""); err != nil { + if err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusProcessing, "", ""); err != nil { s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err)) } driverDeps := s.driverDeps() chainDriver, err := s.driverForNetwork(network.Name.String()) if err != nil { - _, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") + _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") return err } destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination) if err != nil { - _, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") + _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") return err } sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress) if err != nil { - _, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") + _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") return err } if chainDriver.Name() == "tron" && sourceAddress == destinationAddress { @@ -68,7 +68,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet zap.String("wallet_ref", sourceWalletRef), zap.String("network", network.Name.String()), ) - if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusSuccess, "", ""); err != nil { + if err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusSuccess, "", ""); err != nil { s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err)) } return nil @@ -77,13 +77,13 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress) if err != nil { s.logger.Warn("Failed to submit transfer", zap.String("transfer_ref", transferRef), zap.Error(err)) - if _, e := s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), ""); e != nil { + if e := s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), ""); e != nil { s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(e)) } return err } - if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusWaiting, "", txHash); err != nil { + if err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusWaiting, "", txHash); err != nil { s.logger.Warn("Failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err)) } @@ -103,7 +103,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet failureReason = "transaction reverted" pStatus = model.TransferStatusFailed } - if _, err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil { + if err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil { s.logger.Warn("Failed to update transfer status", zap.Error(err), zap.String("transfer_ref", transferRef), zap.String("status", string(pStatus))) } diff --git a/api/gateway/tron/internal/service/gateway/transfer_notifications.go b/api/gateway/tron/internal/service/gateway/transfer_notifications.go index 842a70ee..b6216cca 100644 --- a/api/gateway/tron/internal/service/gateway/transfer_notifications.go +++ b/api/gateway/tron/internal/service/gateway/transfer_notifications.go @@ -57,16 +57,16 @@ func toError(t *model.Transfer) string { return t.FailureReason } -func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) { +func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) error { if !isFinalTransferStatus(status) { - transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash) + _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash) if err != nil { s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err)) } - return transfer, err + return err } - res, err := s.executeTransaction(ctx, func(txCtx context.Context) (any, error) { + _, err := s.executeTransaction(ctx, func(txCtx context.Context) (any, error) { transfer, statusErr := s.storage.Transfers().UpdateStatus(txCtx, transferRef, status, failureReason, txHash) if statusErr != nil { return nil, statusErr @@ -80,11 +80,9 @@ func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, }) if err != nil { s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err)) - return nil, err + return err } - - transfer, _ := res.(*model.Transfer) - return transfer, nil + return nil } func (s *Service) emitTransferStatusEvent(ctx context.Context, transfer *model.Transfer) error { diff --git a/api/gateway/tron/internal/service/gateway/tronclient/client.go b/api/gateway/tron/internal/service/gateway/tronclient/client.go index 617c4b04..1b28b09c 100644 --- a/api/gateway/tron/internal/service/gateway/tronclient/client.go +++ b/api/gateway/tron/internal/service/gateway/tronclient/client.go @@ -157,7 +157,7 @@ func (c *Client) Close() { // SetAPIKey configures the TRON-PRO-API-KEY for TronGrid requests. func (c *Client) SetAPIKey(apiKey string) { if c != nil && c.grpc != nil { - c.grpc.SetAPIKey(apiKey) + _ = c.grpc.SetAPIKey(apiKey) } } diff --git a/api/gateway/tron/shared/hex.go b/api/gateway/tron/shared/hex.go index e33ac582..6dda2155 100644 --- a/api/gateway/tron/shared/hex.go +++ b/api/gateway/tron/shared/hex.go @@ -1,6 +1,7 @@ package shared import ( + "math" "math/big" "strings" @@ -46,5 +47,9 @@ func DecodeHexUint8(input string) (uint8, error) { if val.BitLen() > 8 { return 0, errHexOutOfRange } - return uint8(val.Uint64()), nil + decoded := val.Uint64() + if decoded > math.MaxUint8 { + return 0, errHexOutOfRange + } + return uint8(decoded), nil } diff --git a/api/gateway/tron/storage/mongo/store/wallets.go b/api/gateway/tron/storage/mongo/store/wallets.go index e2e8aa57..a6b4d793 100644 --- a/api/gateway/tron/storage/mongo/store/wallets.go +++ b/api/gateway/tron/storage/mongo/store/wallets.go @@ -190,7 +190,6 @@ func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (* if listErr != nil { if errors.Is(listErr, merrors.ErrNoData) { wallets = make([]model.ManagedWallet, 0) - listErr = nil } else { w.logger.Warn("Wallet list failed", append(fields, zap.Error(listErr))...) return nil, listErr diff --git a/api/ledger/.golangci.yml b/api/ledger/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/ledger/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/ledger/client/client.go b/api/ledger/client/client.go index afb79c49..2590de57 100644 --- a/api/ledger/client/client.go +++ b/api/ledger/client/client.go @@ -843,7 +843,7 @@ func (c *ledgerClient) callContext(ctx context.Context) (context.Context, contex if timeout <= 0 { timeout = 3 * time.Second } - return context.WithTimeout(ctx, timeout) + return context.WithTimeout(ctx, timeout) //nolint:gosec // cancel func is always invoked by call sites } func isLedgerRail(value string) bool { diff --git a/api/ledger/client/client_test.go b/api/ledger/client/client_test.go index faba3c7a..e2c90a1b 100644 --- a/api/ledger/client/client_test.go +++ b/api/ledger/client/client_test.go @@ -20,23 +20,23 @@ type stubConnector struct { } func (s *stubConnector) OpenAccount(context.Context, *connectorv1.OpenAccountRequest, ...grpc.CallOption) (*connectorv1.OpenAccountResponse, error) { - return nil, nil + return &connectorv1.OpenAccountResponse{}, nil } func (s *stubConnector) GetAccount(context.Context, *connectorv1.GetAccountRequest, ...grpc.CallOption) (*connectorv1.GetAccountResponse, error) { - return nil, nil + return &connectorv1.GetAccountResponse{}, nil } func (s *stubConnector) ListAccounts(context.Context, *connectorv1.ListAccountsRequest, ...grpc.CallOption) (*connectorv1.ListAccountsResponse, error) { - return nil, nil + return &connectorv1.ListAccountsResponse{}, nil } func (s *stubConnector) GetBalance(context.Context, *connectorv1.GetBalanceRequest, ...grpc.CallOption) (*connectorv1.GetBalanceResponse, error) { - return nil, nil + return &connectorv1.GetBalanceResponse{}, nil } func (s *stubConnector) UpdateAccountState(context.Context, *connectorv1.UpdateAccountStateRequest, ...grpc.CallOption) (*connectorv1.UpdateAccountStateResponse, error) { - return nil, nil + return &connectorv1.UpdateAccountStateResponse{}, nil } func (s *stubConnector) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest, _ ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error) { @@ -47,11 +47,11 @@ func (s *stubConnector) SubmitOperation(ctx context.Context, req *connectorv1.Su } func (s *stubConnector) GetOperation(context.Context, *connectorv1.GetOperationRequest, ...grpc.CallOption) (*connectorv1.GetOperationResponse, error) { - return nil, nil + return &connectorv1.GetOperationResponse{}, nil } func (s *stubConnector) ListOperations(context.Context, *connectorv1.ListOperationsRequest, ...grpc.CallOption) (*connectorv1.ListOperationsResponse, error) { - return nil, nil + return &connectorv1.ListOperationsResponse{}, nil } func TestTransferInternal_SubmitsTransferOperation(t *testing.T) { diff --git a/api/ledger/internal/model/account.go b/api/ledger/internal/model/account.go index d5b65b7b..8b4b5733 100644 --- a/api/ledger/internal/model/account.go +++ b/api/ledger/internal/model/account.go @@ -106,7 +106,9 @@ func (a *Account) Validate() error { veAdd(&verr, "currentOwners["+strconv.Itoa(i)+"]", "invalid", err.Error()) } } - + if verr == nil { + return nil + } return verr } diff --git a/api/ledger/internal/model/ownership.go b/api/ledger/internal/model/ownership.go index bff539e2..d98a348f 100644 --- a/api/ledger/internal/model/ownership.go +++ b/api/ledger/internal/model/ownership.go @@ -43,6 +43,9 @@ func (o *Ownership) Validate() error { if o.To != nil && o.To.Before(o.From) { veAdd(&verr, "effectiveTo", "before_from", "must be >= effectiveFrom") } + if verr == nil { + return nil + } return verr } diff --git a/api/ledger/internal/model/party.go b/api/ledger/internal/model/party.go index aa3e408a..63399a16 100644 --- a/api/ledger/internal/model/party.go +++ b/api/ledger/internal/model/party.go @@ -72,5 +72,8 @@ func (p *Party) Validate() error { default: veAdd(&verr, "kind", "invalid", "unknown party kind") } + if verr == nil { + return nil + } return verr } diff --git a/api/ledger/internal/service/ledger/account_status.go b/api/ledger/internal/service/ledger/account_status.go index aefa44a3..4bd72949 100644 --- a/api/ledger/internal/service/ledger/account_status.go +++ b/api/ledger/internal/service/ledger/account_status.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "errors" "strings" "github.com/tech/sendico/ledger/storage" @@ -35,7 +36,7 @@ func (s *Service) blockAccountResponder(_ context.Context, req *ledgerv1.BlockAc account, err := s.storage.Accounts().Get(ctx, accountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData("account not found") } logger.Warn("Failed to get account for block", zap.Error(err)) @@ -98,7 +99,7 @@ func (s *Service) unblockAccountResponder(_ context.Context, req *ledgerv1.Unblo account, err := s.storage.Accounts().Get(ctx, accountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData("account not found") } logger.Warn("Failed to get account for unblock", zap.Error(err)) diff --git a/api/ledger/internal/service/ledger/accounts.go b/api/ledger/internal/service/ledger/accounts.go index e6892f64..288b8bd9 100644 --- a/api/ledger/internal/service/ledger/accounts.go +++ b/api/ledger/internal/service/ledger/accounts.go @@ -188,7 +188,7 @@ func (s *Service) persistNewAccount(ctx context.Context, p createAccountParams, // parseOwnerRef parses an optional owner reference string into an ObjectID pointer. func parseOwnerRef(ownerRefStr string) (*bson.ObjectID, error) { if ownerRefStr == "" { - return nil, nil + return nil, nil //nolint:nilnil // empty owner_ref means owner linkage is intentionally omitted } ownerObjID, err := parseObjectID(ownerRefStr) if err != nil { diff --git a/api/ledger/internal/service/ledger/accounts_test.go b/api/ledger/internal/service/ledger/accounts_test.go index 69612be9..eb9e67a1 100644 --- a/api/ledger/internal/service/ledger/accounts_test.go +++ b/api/ledger/internal/service/ledger/accounts_test.go @@ -97,7 +97,7 @@ func (s *accountStoreStub) GetDefaultSettlement(context.Context, bson.ObjectID, } func (s *accountStoreStub) ListByOrganization(context.Context, bson.ObjectID, *storage.AccountsFilter, int, int) ([]*pmodel.LedgerAccount, error) { - return nil, nil + return []*pmodel.LedgerAccount{}, nil } func (s *accountStoreStub) UpdateStatus(context.Context, bson.ObjectID, pmodel.LedgerAccountStatus) error { diff --git a/api/ledger/internal/service/ledger/connector.go b/api/ledger/internal/service/ledger/connector.go index 68e0b12a..d2821171 100644 --- a/api/ledger/internal/service/ledger/connector.go +++ b/api/ledger/internal/service/ledger/connector.go @@ -619,16 +619,27 @@ func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountT case string: return parseLedgerAccountTypeString(v) case float64: - return ledgerv1.AccountType(int32(v)), nil + truncated := int64(v) + if v != float64(truncated) { + return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type") + } + return parseLedgerAccountTypeInt64(truncated) case int: - return ledgerv1.AccountType(v), nil + return parseLedgerAccountTypeInt64(int64(v)) case int64: - return ledgerv1.AccountType(v), nil + return parseLedgerAccountTypeInt64(v) default: return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: account_type is required") } } +func parseLedgerAccountTypeInt64(value int64) (ledgerv1.AccountType, error) { + if value < -1<<31 || value > 1<<31-1 { + return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type") + } + return ledgerv1.AccountType(int32(value)), nil +} + func parseLedgerAccountTypeString(value string) (ledgerv1.AccountType, error) { accountType, ok := ledgerconv.ParseAccountType(value) if !ok || ledgerconv.IsAccountTypeUnspecified(value) { @@ -662,7 +673,7 @@ func parseEventTime(reader params.Reader) *timestamppb.Timestamp { func parseLedgerCharges(reader params.Reader) ([]*ledgerv1.PostingLine, error) { items := reader.List("charges") if len(items) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // nil charges means no extra posting lines were provided } result := make([]*ledgerv1.PostingLine, 0, len(items)) for i, item := range items { diff --git a/api/ledger/internal/service/ledger/external_operations_test.go b/api/ledger/internal/service/ledger/external_operations_test.go index e30d22fa..694beaa9 100644 --- a/api/ledger/internal/service/ledger/external_operations_test.go +++ b/api/ledger/internal/service/ledger/external_operations_test.go @@ -448,7 +448,7 @@ func TestExternalInvariantRandomSequence(t *testing.T) { require.NoError(t, repo.accounts.Create(ctx, pending)) require.NoError(t, repo.accounts.Create(ctx, transit)) - rng := rand.New(rand.NewSource(42)) + rng := rand.New(rand.NewSource(42)) //nolint:gosec // deterministic pseudo-random sequence for invariant stress test for i := 0; i < 50; i++ { switch rng.Intn(3) { case 0: diff --git a/api/ledger/internal/service/ledger/invariant.go b/api/ledger/internal/service/ledger/invariant.go index 267e9e9e..634fd176 100644 --- a/api/ledger/internal/service/ledger/invariant.go +++ b/api/ledger/internal/service/ledger/invariant.go @@ -124,7 +124,9 @@ func (s *Service) listOrganizationAccounts(ctx context.Context, currency string) if err != nil { return nil, err } - defer cursor.Close(ctx) + defer func() { + _ = cursor.Close(ctx) + }() accounts := make([]*pmodel.LedgerAccount, 0) for cursor.Next(ctx) { diff --git a/api/ledger/internal/service/ledger/outbox_reliable.go b/api/ledger/internal/service/ledger/outbox_reliable.go index 302f9f08..cd363b61 100644 --- a/api/ledger/internal/service/ledger/outbox_reliable.go +++ b/api/ledger/internal/service/ledger/outbox_reliable.go @@ -12,8 +12,7 @@ import ( me "github.com/tech/sendico/pkg/messaging/envelope" pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable" "github.com/tech/sendico/pkg/mlogger" - cfgmodel "github.com/tech/sendico/pkg/model" - domainmodel "github.com/tech/sendico/pkg/model" + pmodel "github.com/tech/sendico/pkg/model" notification "github.com/tech/sendico/pkg/model/notification" "github.com/tech/sendico/pkg/mservice" ) @@ -35,7 +34,7 @@ type ledgerOutboxStoreAdapter struct { store storage.OutboxStore } -func newLedgerReliableProducer(logger mlogger.Logger, direct pmessaging.Producer, store storage.OutboxStore, messagingSettings cfgmodel.SettingsT) (*pmessagingreliable.ReliableProducer, pmessagingreliable.Settings, error) { +func newLedgerReliableProducer(logger mlogger.Logger, direct pmessaging.Producer, store storage.OutboxStore, messagingSettings pmodel.SettingsT) (*pmessagingreliable.ReliableProducer, pmessagingreliable.Settings, error) { if store == nil { return nil, pmessagingreliable.DefaultSettings(), nil } @@ -71,7 +70,7 @@ func (a *ledgerOutboxStoreAdapter) Enqueue(ctx context.Context, msg pmessagingre func (a *ledgerOutboxStoreAdapter) ListPending(ctx context.Context, limit int) ([]pmessagingreliable.OutboxMessage, error) { if a == nil || a.store == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil adapter/store means no pending outbox messages } events, err := a.store.ListPending(ctx, limit) if err != nil { @@ -160,7 +159,7 @@ func buildLedgerOutboxEnvelope(eventID string, payload []byte, attempts int, org return nil, err } - env := me.CreateEnvelope(outboxPublisherSender, domainmodel.NewNotification(mservice.LedgerOutbox, notification.NASent)) + env := me.CreateEnvelope(outboxPublisherSender, pmodel.NewNotification(mservice.LedgerOutbox, notification.NASent)) if _, err = env.Wrap(body); err != nil { return nil, err } diff --git a/api/ledger/internal/service/ledger/posting.go b/api/ledger/internal/service/ledger/posting.go index a8ca63b1..9393a893 100644 --- a/api/ledger/internal/service/ledger/posting.go +++ b/api/ledger/internal/service/ledger/posting.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "errors" "fmt" "strings" "time" @@ -73,7 +74,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi EntryType: ledgerv1.EntryType_ENTRY_CREDIT, }, nil } - if err != nil && err != storage.ErrJournalEntryNotFound { + if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) { recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorIdempotencyCheck) logger.Warn("Failed to check idempotency", zap.Error(err)) return nil, merrors.Internal("failed to check idempotency") @@ -122,7 +123,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) } logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) diff --git a/api/ledger/internal/service/ledger/posting_debit.go b/api/ledger/internal/service/ledger/posting_debit.go index eea05f71..2d7bcd2e 100644 --- a/api/ledger/internal/service/ledger/posting_debit.go +++ b/api/ledger/internal/service/ledger/posting_debit.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "errors" "fmt" "strings" "time" @@ -71,7 +72,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR EntryType: ledgerv1.EntryType_ENTRY_DEBIT, }, nil } - if err != nil && err != storage.ErrJournalEntryNotFound { + if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) { logger.Warn("Failed to check idempotency", zap.Error(err)) return nil, merrors.Internal("failed to check idempotency") } @@ -119,7 +120,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) } logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef)) diff --git a/api/ledger/internal/service/ledger/posting_external.go b/api/ledger/internal/service/ledger/posting_external.go index 12397810..d970147d 100644 --- a/api/ledger/internal/service/ledger/posting_external.go +++ b/api/ledger/internal/service/ledger/posting_external.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "errors" "fmt" "strings" "time" @@ -69,7 +70,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P EntryType: ledgerv1.EntryType_ENTRY_CREDIT, }, nil } - if err != nil && err != storage.ErrJournalEntryNotFound { + if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) { recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorIdempotencyCheck) logger.Warn("Failed to check idempotency", zap.Error(err)) return nil, merrors.Internal("failed to check idempotency") @@ -137,7 +138,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) } logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef)) @@ -294,7 +295,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po EntryType: ledgerv1.EntryType_ENTRY_DEBIT, }, nil } - if err != nil && err != storage.ErrJournalEntryNotFound { + if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) { recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorIdempotencyCheck) logger.Warn("Failed to check idempotency", zap.Error(err)) return nil, merrors.Internal("failed to check idempotency") @@ -362,7 +363,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) } logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef)) diff --git a/api/ledger/internal/service/ledger/posting_fx.go b/api/ledger/internal/service/ledger/posting_fx.go index 5f7d0a79..fb0acd8a 100644 --- a/api/ledger/internal/service/ledger/posting_fx.go +++ b/api/ledger/internal/service/ledger/posting_fx.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "errors" "fmt" "time" @@ -84,7 +85,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp EntryType: ledgerv1.EntryType_ENTRY_FX, }, nil } - if err != nil && err != storage.ErrJournalEntryNotFound { + if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) { logger.Warn("Failed to check idempotency", zap.Error(err)) return nil, merrors.Internal("failed to check idempotency") } @@ -92,7 +93,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp // Verify both accounts exist and are active fromAccount, err := s.storage.Accounts().Get(ctx, fromAccountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData("from_account not found") } logger.Warn("Failed to get from_account", zap.Error(err)) @@ -104,7 +105,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp toAccount, err := s.storage.Accounts().Get(ctx, toAccountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData("to_account not found") } logger.Warn("Failed to get to_account", zap.Error(err)) @@ -158,7 +159,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) } logger.Warn("Failed to get FX charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef)) diff --git a/api/ledger/internal/service/ledger/posting_transfer.go b/api/ledger/internal/service/ledger/posting_transfer.go index 969af2e8..2a81d690 100644 --- a/api/ledger/internal/service/ledger/posting_transfer.go +++ b/api/ledger/internal/service/ledger/posting_transfer.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "errors" "fmt" "strings" "time" @@ -94,7 +95,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq EntryType: ledgerv1.EntryType_ENTRY_TRANSFER, }, nil } - if err != nil && err != storage.ErrJournalEntryNotFound { + if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) { logger.Warn("Failed to check idempotency", zap.Error(err)) return nil, merrors.Internal("failed to check idempotency") } @@ -168,7 +169,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) } logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef)) diff --git a/api/ledger/internal/service/ledger/queries.go b/api/ledger/internal/service/ledger/queries.go index 670806ae..6e885c04 100644 --- a/api/ledger/internal/service/ledger/queries.go +++ b/api/ledger/internal/service/ledger/queries.go @@ -3,6 +3,7 @@ package ledger import ( "context" "encoding/base64" + "errors" "fmt" "strconv" "strings" @@ -33,7 +34,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc // Get account to verify it exists account, err := s.storage.Accounts().Get(ctx, accountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData("account not found") } logger.Warn("Failed to get account", zap.Error(err)) @@ -43,7 +44,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc // Get balance balance, err := s.storage.Balances().Get(ctx, accountRef) if err != nil { - if err == storage.ErrBalanceNotFound { + if errors.Is(err, storage.ErrBalanceNotFound) { // Return zero balance if account exists but has no balance yet return &ledgerv1.BalanceResponse{ LedgerAccountRef: req.LedgerAccountRef, @@ -89,7 +90,7 @@ func (s *Service) getJournalEntryResponder(_ context.Context, req *ledgerv1.GetE // Get journal entry entry, err := s.storage.JournalEntries().Get(ctx, entryRef) if err != nil { - if err == storage.ErrJournalEntryNotFound { + if errors.Is(err, storage.ErrJournalEntryNotFound) { return nil, merrors.NoData("journal entry not found") } logger.Warn("Failed to get journal entry", zap.Error(err)) @@ -148,7 +149,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat // Verify account exists _, err = s.storage.Accounts().Get(ctx, accountRef) if err != nil { - if err == storage.ErrAccountNotFound { + if errors.Is(err, storage.ErrAccountNotFound) { return nil, merrors.NoData("account not found") } logger.Warn("Failed to get account", zap.Error(err)) diff --git a/api/ledger/internal/service/ledger/service.go b/api/ledger/internal/service/ledger/service.go index 9f195df6..654cf609 100644 --- a/api/ledger/internal/service/ledger/service.go +++ b/api/ledger/internal/service/ledger/service.go @@ -415,7 +415,7 @@ func (s *Service) startDiscoveryAnnouncer() { InvokeURI: s.invokeURI, Version: appversion.Create().Short(), } - s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.Ledger), announce) + s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.Ledger, announce) s.announcer.Start() } @@ -446,7 +446,7 @@ func (s *Service) startOutboxReliableProducer() error { zap.Int("poll_interval_seconds", settings.PollIntervalSeconds), zap.Int("max_attempts", settings.MaxAttempts)) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // canceled explicitly in Shutdown s.outbox.cancel = cancel go s.outbox.producer.Run(ctx) }) diff --git a/api/ledger/storage/mongo/store/testing_helpers_test.go b/api/ledger/storage/mongo/store/testing_helpers_test.go index 1b0f9ef8..73707984 100644 --- a/api/ledger/storage/mongo/store/testing_helpers_test.go +++ b/api/ledger/storage/mongo/store/testing_helpers_test.go @@ -118,15 +118,15 @@ func (r *repositoryStub) ListIDs(ctx context.Context, query builder.Query) ([]bs if r.ListIDsFunc != nil { return r.ListIDsFunc(ctx, query) } - return nil, nil + return []bson.ObjectID{}, nil } func (r *repositoryStub) ListPermissionBound(ctx context.Context, query builder.Query) ([]model.PermissionBoundStorable, error) { - return nil, nil + return []model.PermissionBoundStorable{}, nil } func (r *repositoryStub) ListAccountBound(ctx context.Context, query builder.Query) ([]model.AccountBoundStorable, error) { - return nil, nil + return []model.AccountBoundStorable{}, nil } func (r *repositoryStub) Collection() string { diff --git a/api/notification/.golangci.yml b/api/notification/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/notification/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/notification/internal/server/amplitude/nsent.go b/api/notification/internal/server/amplitude/nsent.go deleted file mode 100644 index 495ba761..00000000 --- a/api/notification/internal/server/amplitude/nsent.go +++ /dev/null @@ -1,16 +0,0 @@ -package ampliimp - -import ( - "context" - - "github.com/tech/sendico/notification/internal/ampli" - "github.com/tech/sendico/pkg/model" -) - -func (a *AmplitudeAPI) onNotificationSent(_ context.Context, nresult *model.NotificationResult) error { - ampli.Instance.EmailSent( - nresult.UserID, - ampli.EmailSent.Builder().Domain("").EmailType("").Build(), - ) - return nil -} diff --git a/api/notification/internal/server/notificationimp/confirmation_request.go b/api/notification/internal/server/notificationimp/confirmation_request.go index b92069c6..31963808 100644 --- a/api/notification/internal/server/notificationimp/confirmation_request.go +++ b/api/notification/internal/server/notificationimp/confirmation_request.go @@ -58,7 +58,7 @@ func (a *NotificationAPI) onConfirmationRequest(ctx context.Context, request *mo SourceService: req.SourceService, Rail: req.Rail, } - env := confirmations.ConfirmationDispatch(string(mservice.Notifications), dispatch, req.SourceService, req.Rail) + env := confirmations.ConfirmationDispatch(mservice.Notifications, dispatch, req.SourceService, req.Rail) if err := a.producer.SendMessage(env); err != nil { a.logger.Warn("Failed to publish confirmation dispatch", zap.Error(err), zap.String("request_id", req.RequestID), zap.String("message_id", dispatch.MessageID)) return err @@ -143,7 +143,7 @@ func confirmationPrompt(req *model.ConfirmationRequest) string { if err != nil { amountFloat = 0 } - builder.WriteString(fmt.Sprintf("\n*Requested: %.2f %s*\n\n", amountFloat, req.RequestedMoney.Currency)) + _, _ = fmt.Fprintf(&builder, "\n*Requested: %.2f %s*\n\n", amountFloat, req.RequestedMoney.Currency) } builder.WriteString("Reply with \" \" (e.g., 12.34 USD).") return builder.String() diff --git a/api/notification/internal/server/notificationimp/mail/internal/mailimp.go b/api/notification/internal/server/notificationimp/mail/internal/mailimp.go index 65a6df43..5778cb2a 100755 --- a/api/notification/internal/server/notificationimp/mail/internal/mailimp.go +++ b/api/notification/internal/server/notificationimp/mail/internal/mailimp.go @@ -148,9 +148,8 @@ func NewClient(logger mlogger.Logger, l localizer.Localizer, dp domainprovider.D // Timeout for send the data and wait respond smtpServer.SendTimeout = mduration.Param2Duration(config.TimeOut, time.Second) - // Set TLSConfig to provide custom TLS configuration. For example, - // to skip TLS verification (useful for testing): - smtpServer.TLSConfig = &tls.Config{InsecureSkipVerify: true} + // Keep certificate verification enabled. + smtpServer.TLSConfig = &tls.Config{InsecureSkipVerify: false} // SMTP client lg := logger.Named("client") diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index 5e1438b0..19bf699e 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -123,7 +123,7 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { Operations: []string{discovery.OperationNotifySend}, Version: appversion.Create().Short(), } - p.announcer = discovery.NewAnnouncer(p.logger, a.Register().Producer(), string(mservice.Notifications), announce) + p.announcer = discovery.NewAnnouncer(p.logger, a.Register().Producer(), mservice.Notifications, announce) p.announcer.Start() return p, nil diff --git a/api/notification/internal/server/notificationimp/notification_test.go b/api/notification/internal/server/notificationimp/notification_test.go index c5052127..8eeb1e35 100644 --- a/api/notification/internal/server/notificationimp/notification_test.go +++ b/api/notification/internal/server/notificationimp/notification_test.go @@ -28,7 +28,6 @@ type mockSentMessage struct { locale string recipients []string data map[string]string - buttonLink string } func (m *mockMailClient) Send(r mmail.MailBuilder) error { diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index 7472d990..c1370e13 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -186,7 +186,11 @@ func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) (* c.logger.Warn("Telegram request failed", zap.Error(err)) return nil, err } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + c.logger.Warn("Failed to close telegram response body", zap.Error(closeErr)) + } + }() respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<10)) if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { @@ -321,7 +325,11 @@ func (c *client) sendReaction(ctx context.Context, payload setMessageReactionPay c.logger.Warn("Telegram reaction request failed", zap.Error(err)) return err } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + c.logger.Warn("Failed to close telegram reaction response body", zap.Error(closeErr)) + } + }() respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<10)) if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { diff --git a/api/notification/internal/server/notificationimp/webhook.go b/api/notification/internal/server/notificationimp/webhook.go index 97dcf168..1f5cf125 100644 --- a/api/notification/internal/server/notificationimp/webhook.go +++ b/api/notification/internal/server/notificationimp/webhook.go @@ -61,7 +61,7 @@ func (a *NotificationAPI) handleTelegramWebhook(w http.ResponseWriter, r *http.R if update.Message != nil { payload.Message = update.Message.ToModel() } - env := tnotifications.TelegramUpdate(string(mservice.Notifications), payload) + env := tnotifications.TelegramUpdate(mservice.Notifications, payload) if err := a.producer.SendMessage(env); err != nil { if a.logger != nil { a.logger.Warn("Failed to publish telegram webhook update", zap.Error(err), zap.Int64("update_id", update.UpdateID)) diff --git a/api/notification/main.go b/api/notification/main.go index 3e5bd4f9..15fff5bf 100644 --- a/api/notification/main.go +++ b/api/notification/main.go @@ -9,8 +9,8 @@ import ( ) // generate translations -// go:generate Users/stephandeshevikh/go/bin/go18n extract -// go:generate Users/stephandeshevikh/go/bin/go18n merge +//go:generate Users/stephandeshevikh/go/bin/go18n extract +//go:generate Users/stephandeshevikh/go/bin/go18n merge // lint go code // docker run -t --rm -v $(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v --timeout 10m0s --enable-all -D ireturn -D wrapcheck -D varnamelen -D tagliatelle -D nosnakecase -D gochecknoglobals -D nlreturn -D stylecheck -D lll -D wsl -D scopelint -D varcheck -D exhaustivestruct -D golint -D maligned -D interfacer -D ifshort -D structcheck -D deadcode -D godot -D depguard -D tagalign diff --git a/api/payments/methods/.golangci.yml b/api/payments/methods/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/payments/methods/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/payments/methods/client/client.go b/api/payments/methods/client/client.go index 130007ad..56978920 100644 --- a/api/payments/methods/client/client.go +++ b/api/payments/methods/client/client.go @@ -85,52 +85,86 @@ func (c *paymentMethodsClient) Close() error { return nil } -func (c *paymentMethodsClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { +func (c *paymentMethodsClient) CreatePaymentMethod(ctx context.Context, req *methodsv1.CreatePaymentMethodRequest) (*methodsv1.CreatePaymentMethodResponse, error) { timeout := c.cfg.CallTimeout if timeout <= 0 { timeout = 3 * time.Second } - return context.WithTimeout(ctx, timeout) -} -func (c *paymentMethodsClient) CreatePaymentMethod(ctx context.Context, req *methodsv1.CreatePaymentMethodRequest) (*methodsv1.CreatePaymentMethodResponse, error) { - callCtx, cancel := c.callContext(ctx) + callCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + return c.client.CreatePaymentMethod(callCtx, req) } func (c *paymentMethodsClient) GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymentMethodRequest) (*methodsv1.GetPaymentMethodResponse, error) { - callCtx, cancel := c.callContext(ctx) + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + + callCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + return c.client.GetPaymentMethod(callCtx, req) } func (c *paymentMethodsClient) GetPaymentMethodPrivate(ctx context.Context, req *methodsv1.GetPaymentMethodPrivateRequest) (*methodsv1.GetPaymentMethodPrivateResponse, error) { - callCtx, cancel := c.callContext(ctx) + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + + callCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + return c.client.GetPaymentMethodPrivate(callCtx, req) } func (c *paymentMethodsClient) UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error) { - callCtx, cancel := c.callContext(ctx) + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + + callCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + return c.client.UpdatePaymentMethod(callCtx, req) } func (c *paymentMethodsClient) DeletePaymentMethod(ctx context.Context, req *methodsv1.DeletePaymentMethodRequest) (*methodsv1.DeletePaymentMethodResponse, error) { - callCtx, cancel := c.callContext(ctx) + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + + callCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + return c.client.DeletePaymentMethod(callCtx, req) } func (c *paymentMethodsClient) SetPaymentMethodArchived(ctx context.Context, req *methodsv1.SetPaymentMethodArchivedRequest) (*methodsv1.SetPaymentMethodArchivedResponse, error) { - callCtx, cancel := c.callContext(ctx) + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + + callCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + return c.client.SetPaymentMethodArchived(callCtx, req) } func (c *paymentMethodsClient) ListPaymentMethods(ctx context.Context, req *methodsv1.ListPaymentMethodsRequest) (*methodsv1.ListPaymentMethodsResponse, error) { - callCtx, cancel := c.callContext(ctx) + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + + callCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + return c.client.ListPaymentMethods(callCtx, req) } diff --git a/api/payments/orchestrator/.golangci.yml b/api/payments/orchestrator/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/payments/orchestrator/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/payments/orchestrator/client/client.go b/api/payments/orchestrator/client/client.go index aa4229f5..fbdeeb43 100644 --- a/api/payments/orchestrator/client/client.go +++ b/api/payments/orchestrator/client/client.go @@ -122,5 +122,5 @@ func (c *orchestratorClient) callContext(ctx context.Context) (context.Context, if timeout <= 0 { timeout = 3 * time.Second } - return context.WithTimeout(ctx, timeout) + return context.WithTimeout(ctx, timeout) //nolint:gosec // cancel func is always invoked by call sites } diff --git a/api/payments/orchestrator/internal/server/internal/discovery.go b/api/payments/orchestrator/internal/server/internal/discovery.go index 0fc1be49..4cfc68a7 100644 --- a/api/payments/orchestrator/internal/server/internal/discovery.go +++ b/api/payments/orchestrator/internal/server/internal/discovery.go @@ -37,7 +37,7 @@ func (i *Imp) initDiscovery(cfg *config) { InvokeURI: cfg.GRPC.DiscoveryInvokeURI(), Version: appversion.Create().Short(), } - i.discoveryAnnouncer = discovery.NewAnnouncer(i.logger, producer, string(mservice.PaymentOrchestrator), announce) + i.discoveryAnnouncer = discovery.NewAnnouncer(i.logger, producer, mservice.PaymentOrchestrator, announce) i.discoveryAnnouncer.Start() } diff --git a/api/payments/orchestrator/internal/server/internal/discovery_clients.go b/api/payments/orchestrator/internal/server/internal/discovery_clients.go index 6e3fa5ff..694000b6 100644 --- a/api/payments/orchestrator/internal/server/internal/discovery_clients.go +++ b/api/payments/orchestrator/internal/server/internal/discovery_clients.go @@ -321,7 +321,7 @@ func (r *discoveryClientResolver) findEntry(service mservice.Type, rail string, } func discoverySelectionKey(service mservice.Type, rail, network string) string { - serviceName := strings.TrimSpace(string(service)) + serviceName := strings.TrimSpace(service) railName := strings.ToUpper(strings.TrimSpace(rail)) networkName := strings.ToUpper(strings.TrimSpace(network)) @@ -392,10 +392,10 @@ func discoveryEntryKey(entry discovery.RegistryEntry) string { func matchesService(service string, candidate mservice.Type) bool { service = strings.TrimSpace(service) - if service == "" || strings.TrimSpace(string(candidate)) == "" { + if service == "" || strings.TrimSpace(candidate) == "" { return false } - return strings.EqualFold(service, strings.TrimSpace(string(candidate))) + return strings.EqualFold(service, strings.TrimSpace(candidate)) } func parseDiscoveryEndpoint(raw string) (discoveryEndpoint, error) { @@ -441,15 +441,12 @@ func parseDiscoveryEndpoint(raw string) (discoveryEndpoint, error) { } } -func dialGrpc(ctx context.Context, endpoint discoveryEndpoint) (*grpc.ClientConn, error) { +func dialGrpc(_ context.Context, endpoint discoveryEndpoint) (*grpc.ClientConn, error) { dialOpts := []grpc.DialOption{} if endpoint.insecure { dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) } else { dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(nil))) } - if ctx == nil { - ctx = context.Background() - } return grpc.NewClient(endpoint.address, dialOpts...) } diff --git a/api/payments/orchestrator/internal/server/internal/discovery_wrappers.go b/api/payments/orchestrator/internal/server/internal/discovery_wrappers.go index c80c5406..1ca043f5 100644 --- a/api/payments/orchestrator/internal/server/internal/discovery_wrappers.go +++ b/api/payments/orchestrator/internal/server/internal/discovery_wrappers.go @@ -203,7 +203,7 @@ func (c *discoveryLedgerClient) Close() error { } client, err := c.resolver.LedgerClient(context.Background()) if err != nil { - return nil + return err } return client.Close() } @@ -241,7 +241,7 @@ func (c *discoveryOracleClient) Close() error { } client, err := c.resolver.OracleClient(context.Background()) if err != nil { - return nil + return err } return client.Close() } @@ -348,7 +348,7 @@ func (c *discoveryChainClient) Close() error { } client, err := c.resolver.ChainClient(context.Background(), c.invokeURI) if err != nil { - return nil + return err } return client.Close() } diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/agg/service.go b/api/payments/orchestrator/internal/service/orchestrationv2/agg/service.go index a6104db6..186423a6 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/agg/service.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/agg/service.go @@ -172,7 +172,7 @@ func cloneIntentSnapshot(src model.PaymentIntent) (model.PaymentIntent, error) { func cloneQuoteSnapshot(src *model.PaymentQuoteSnapshot) (*model.PaymentQuoteSnapshot, error) { if src == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil snapshot means quote data is intentionally absent } dst := &model.PaymentQuoteSnapshot{} if err := bsonClone(src, dst); err != nil { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/agg/service_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/agg/service_test.go index e4946fd5..66c915bc 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/agg/service_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/agg/service_test.go @@ -291,7 +291,6 @@ func TestCreate_InputValidation(t *testing.T) { factory := New() for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { _, err := factory.Create(tt.in) if err == nil { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/oobs/audit_store.go b/api/payments/orchestrator/internal/service/orchestrationv2/oobs/audit_store.go index a88ede5f..fe299351 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/oobs/audit_store.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/oobs/audit_store.go @@ -95,8 +95,8 @@ func paginateEntries(items []TimelineEntry, limit int32, desc bool) []TimelineEn if len(items) == 0 { return nil } - if int32(len(items)) > limit { - items = items[:limit] + if limit > 0 && len(items) > int(limit) { + items = items[:int(limit)] } out := make([]TimelineEntry, 0, len(items)) for i := range items { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/clone.go b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/clone.go index dee1ed58..0f48cce9 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/clone.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/clone.go @@ -229,7 +229,7 @@ func cloneIntentSnapshot(src model.PaymentIntent) (model.PaymentIntent, error) { func cloneQuoteSnapshot(src *model.PaymentQuoteSnapshot) (*model.PaymentQuoteSnapshot, error) { if src == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil snapshot means quote data is intentionally absent } dst := &model.PaymentQuoteSnapshot{} if err := bsonClone(src, dst); err != nil { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/pquery/service.go b/api/payments/orchestrator/internal/service/orchestrationv2/pquery/service.go index 18c4bd4a..ca6e72ff 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/pquery/service.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/pquery/service.go @@ -360,11 +360,11 @@ func buildOutput(items []*agg.Payment, limit int32) *ListPaymentsOutput { if len(items) == 0 { return &ListPaymentsOutput{} } - if int32(len(items)) > limit { - items = items[:limit] + if limit > 0 && len(items) > int(limit) { + items = items[:int(limit)] } var nextCursor *prepo.ListCursor - if int32(len(items)) == limit { + if limit > 0 && len(items) == int(limit) { nextCursor = cursorFromPayment(items[len(items)-1]) } return &ListPaymentsOutput{ diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go index 8e316323..c1dac99e 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go @@ -32,7 +32,7 @@ func (*paymentDocument) Collection() string { func toDocument(payment *agg.Payment) (*paymentDocument, error) { if payment == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil aggregate means no document to persist } doc := &paymentDocument{ Base: payment.Base, @@ -52,7 +52,7 @@ func toDocument(payment *agg.Payment) (*paymentDocument, error) { func fromDocument(doc *paymentDocument) (*agg.Payment, error) { if doc == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil document means no aggregate to hydrate } cloned, err := cloneDocument(doc) if err != nil { @@ -75,7 +75,7 @@ func fromDocument(doc *paymentDocument) (*agg.Payment, error) { func cloneDocument(doc *paymentDocument) (*paymentDocument, error) { if doc == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil document means no clone result } data, err := bson.Marshal(doc) if err != nil { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/mongo_store.go b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/mongo_store.go index 02f2fff4..d8389c0e 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/mongo_store.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/mongo_store.go @@ -2,6 +2,7 @@ package prepo import ( "context" + "errors" "strings" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" @@ -62,7 +63,7 @@ func (s *mongoStore) EnsureIndexes(defs []*indexDefinition) error { opt.SetSparse(true) } if def.TTL != nil { - opt.SetExpireAfterSeconds(int32(*def.TTL)) + opt.SetExpireAfterSeconds(*def.TTL) } if def.PartialFilter != nil { opt.SetPartialFilterExpression(def.PartialFilter.BuildQuery()) @@ -174,7 +175,7 @@ func (s *mongoStore) findOne(ctx context.Context, filter bson.D) (*paymentDocume if err == nil { return doc, nil } - if err == mongo.ErrNoDocuments { + if errors.Is(err, mongo.ErrNoDocuments) { return nil, ErrPaymentNotFound } return nil, err @@ -206,7 +207,9 @@ func (s *mongoStore) list(ctx context.Context, filter bson.D, cursor *listCursor if err != nil { return nil, err } - defer cur.Close(ctx) + defer func() { + _ = cur.Close(ctx) + }() items := make([]*paymentDocument, 0) for cur.Next(ctx) { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/service.go b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/service.go index 9191d418..44a6961c 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/service.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/service.go @@ -587,7 +587,7 @@ func normalizeStepState(state agg.StepState) (agg.StepState, bool) { func normalizeCursor(cursor *ListCursor) (*listCursor, error) { if cursor == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil cursor means pagination starts from first page } if cursor.ID.IsZero() { return nil, merrors.InvalidArgument("cursor.id is required") diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/prmap/service.go b/api/payments/orchestrator/internal/service/orchestrationv2/prmap/service.go index 02b32028..992a2368 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/prmap/service.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/prmap/service.go @@ -54,7 +54,7 @@ func (s *svc) Map(in MapInput) (out *MapOutput, err error) { func mapPayment(src *agg.Payment) (*orchestrationv2.Payment, error) { if src == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil aggregate means no payment to map } intentSnapshot, err := mapIntentSnapshot(src.IntentSnapshot) diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/aggregate_state.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/aggregate_state.go index 2dad8b01..faad6a2e 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/aggregate_state.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/aggregate_state.go @@ -100,8 +100,8 @@ func (s *svc) transitionAggregateState(current, target agg.State) (agg.State, bo if current == target { return current, true, nil } - if err := s.state.EnsureAggregateTransition(current, target); err != nil { - return current, false, nil + if s.state.EnsureAggregateTransition(current, target) != nil { + return current, false, nil //nolint:nilerr // invalid transition is treated as a no-op state update } return target, true, nil } diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch.go index faaee287..fb830661 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch.go @@ -381,8 +381,5 @@ func routeContainsCardPayout(snapshot *model.PaymentQuoteSnapshot) bool { return true } } - if model.ParseRail(snapshot.Route.Rail) == discovery.RailCardPayout { - return true - } - return false + return model.ParseRail(snapshot.Route.Rail) == discovery.RailCardPayout } diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/query.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/query.go index 361d9920..b4f137ea 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/query.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/query.go @@ -141,7 +141,7 @@ func (s *svc) ListPayments(ctx context.Context, req *orchestrationv2.ListPayment func (s *svc) mapPayments(items []*agg.Payment) ([]*orchestrationv2.Payment, error) { if len(items) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // nil slice means no payments in current page } out := make([]*orchestrationv2.Payment, 0, len(items)) for i := range items { @@ -160,7 +160,7 @@ func (s *svc) mapPayment(payment *agg.Payment) (*orchestrationv2.Payment, error) return nil, err } if mapped == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil mapped output means mapper produced no payment payload } return mapped.Payment, nil } diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/request_helpers.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/request_helpers.go index 553db795..789c7ed0 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/request_helpers.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/request_helpers.go @@ -46,7 +46,7 @@ func parsePaymentRef(value string) (string, error) { func parseCursor(value string) (*prepo.ListCursor, error) { raw := strings.TrimSpace(value) if raw == "" { - return nil, nil + return nil, nil //nolint:nilnil // empty cursor means pagination starts from first page } data, err := base64.RawURLEncoding.DecodeString(raw) if err != nil { @@ -86,7 +86,7 @@ func formatCursor(cursor *prepo.ListCursor) (string, error) { func parseCreated(ts *timestamppb.Timestamp, field string) (*time.Time, error) { if ts == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil timestamp means the filter boundary is not provided } if err := ts.CheckValid(); err != nil { return nil, merrors.InvalidArgument(field + " is invalid") @@ -97,7 +97,7 @@ func parseCreated(ts *timestamppb.Timestamp, field string) (*time.Time, error) { func mapStates(states []orchestrationv2.OrchestrationState) ([]agg.State, error) { if len(states) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // empty state filter means all states are allowed } out := make([]agg.State, 0, len(states)) for i := range states { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/qsnap/resolve_errors_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/qsnap/resolve_errors_test.go index a868ac93..85fd6344 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/qsnap/resolve_errors_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/qsnap/resolve_errors_test.go @@ -183,7 +183,6 @@ func TestResolve_InputValidation(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { _, err := resolver.Resolve(context.Background(), tt.store, tt.in) if err == nil { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/qsnap/service.go b/api/payments/orchestrator/internal/service/orchestrationv2/qsnap/service.go index 5080bec8..66ca833e 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/qsnap/service.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/qsnap/service.go @@ -290,7 +290,7 @@ func cloneIntentSnapshot(src model.PaymentIntent) (model.PaymentIntent, error) { func cloneQuoteSnapshot(src *model.PaymentQuoteSnapshot) (*model.PaymentQuoteSnapshot, error) { if src == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil snapshot means quote data is intentionally absent } dst := &model.PaymentQuoteSnapshot{} if err := bsonClone(src, dst); err != nil { @@ -301,7 +301,7 @@ func cloneQuoteSnapshot(src *model.PaymentQuoteSnapshot) (*model.PaymentQuoteSna func cloneStatusSnapshot(src *model.QuoteStatusV2) (*model.QuoteStatusV2, error) { if src == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil snapshot means status data is intentionally absent } dst := &model.QuoteStatusV2{} if err := bsonClone(src, dst); err != nil { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/reqval/validator_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/reqval/validator_test.go index f6d27a43..ef517727 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/reqval/validator_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/reqval/validator_test.go @@ -200,7 +200,6 @@ func TestValidate_Errors(t *testing.T) { v := New() for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { ctx, err := v.Validate(tt.req) if err == nil { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_policy_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_policy_test.go index 4d5d0c2b..77810cbd 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_policy_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_policy_test.go @@ -209,7 +209,6 @@ func TestCompile_ValidationErrors(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { _, err := compiler.Compile(tt.in) if !errors.Is(err, merrors.ErrInvalidArg) { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/test_helpers_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/test_helpers_test.go index 01a32834..b4a4b804 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/test_helpers_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/test_helpers_test.go @@ -1,6 +1,7 @@ package xplan import ( + "slices" "testing" "github.com/tech/sendico/payments/storage/model" @@ -45,13 +46,5 @@ func railPtr(v model.Rail) *model.Rail { } func equalStringSlice(a, b []string) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true + return slices.Equal(a, b) } diff --git a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go index 759ac482..103d0fde 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go @@ -219,7 +219,7 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_SubmitsWalletFeeTransferOnSend(t *t }, nil default: t.Fatalf("unexpected transfer submission call %d", len(submitRequests)) - return nil, nil + panic("unreachable") } }, } @@ -366,7 +366,7 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_ResolvesFeeAddressFromFeeWalletRef( }, nil default: t.Fatalf("unexpected transfer submission call %d", len(submitRequests)) - return nil, nil + panic("unreachable") } }, } diff --git a/api/payments/quotation/.golangci.yml b/api/payments/quotation/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/payments/quotation/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/payments/quotation/internal/server/internal/discovery_clients.go b/api/payments/quotation/internal/server/internal/discovery_clients.go index 57efa465..170d5109 100644 --- a/api/payments/quotation/internal/server/internal/discovery_clients.go +++ b/api/payments/quotation/internal/server/internal/discovery_clients.go @@ -226,7 +226,7 @@ func (r *discoveryClientResolver) findLedgerEntry() (*discovery.RegistryEntry, b entries := r.registry.List(time.Now(), true) matches := make([]discovery.RegistryEntry, 0) for _, entry := range entries { - if !strings.EqualFold(strings.TrimSpace(entry.Service), string(mservice.Ledger)) { + if !strings.EqualFold(strings.TrimSpace(entry.Service), mservice.Ledger) { continue } if strings.TrimSpace(entry.InvokeURI) == "" { diff --git a/api/payments/quotation/internal/service/plan/builder.go b/api/payments/quotation/internal/service/plan/builder.go index 9ad317a8..f6729d48 100644 --- a/api/payments/quotation/internal/service/plan/builder.go +++ b/api/payments/quotation/internal/service/plan/builder.go @@ -38,7 +38,7 @@ func SendDirectionForRail(rail model.Rail) SendDirection { } func IsGatewayEligible(gw *model.GatewayInstanceDescriptor, rail model.Rail, network, currency string, action model.RailOperation, dir SendDirection, amount decimal.Decimal) error { - return model.IsGatewayEligible(gw, rail, network, currency, action, toGatewayDirection(sendDirection(dir)), amount) + return model.IsGatewayEligible(gw, rail, network, currency, action, toGatewayDirection(dir), amount) } func ParseRailValue(value string) model.Rail { diff --git a/api/payments/quotation/internal/service/plan/helpers.go b/api/payments/quotation/internal/service/plan/helpers.go index 127f84e6..59bdbfae 100644 --- a/api/payments/quotation/internal/service/plan/helpers.go +++ b/api/payments/quotation/internal/service/plan/helpers.go @@ -324,7 +324,7 @@ func makeMoney(currency string, value decimal.Decimal) *moneyv1.Money { func ensureCurrency(m *moneyv1.Money, targetCurrency string, quote *oraclev1.Quote) (*moneyv1.Money, error) { if m == nil || strings.TrimSpace(targetCurrency) == "" { - return nil, nil + return nil, nil //nolint:nilnil // nil means no amount available for conversion } if strings.EqualFold(m.GetCurrency(), targetCurrency) { return cloneProtoMoney(m), nil @@ -334,12 +334,12 @@ func ensureCurrency(m *moneyv1.Money, targetCurrency string, quote *oraclev1.Quo func convertWithQuote(m *moneyv1.Money, quote *oraclev1.Quote, targetCurrency string) (*moneyv1.Money, error) { if m == nil || quote == nil || quote.GetPair() == nil || quote.GetPrice() == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil means conversion cannot be performed with available quote } base := strings.TrimSpace(quote.GetPair().GetBase()) qt := strings.TrimSpace(quote.GetPair().GetQuote()) if base == "" || qt == "" || strings.TrimSpace(targetCurrency) == "" { - return nil, nil + return nil, nil //nolint:nilnil // nil means conversion cannot be performed with incomplete pair } price, err := decimal.NewFromString(quote.GetPrice().GetValue()) if err != nil || price.IsZero() { @@ -355,7 +355,7 @@ func convertWithQuote(m *moneyv1.Money, quote *oraclev1.Quote, targetCurrency st case strings.EqualFold(m.GetCurrency(), qt) && strings.EqualFold(targetCurrency, base): return makeMoney(targetCurrency, value.Div(price)), nil default: - return nil, nil + return nil, nil //nolint:nilnil // nil means quote pair does not match requested conversion } } diff --git a/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_gate_builder.go b/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_gate_builder.go index 0d656186..e96d209a 100644 --- a/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_gate_builder.go +++ b/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_gate_builder.go @@ -15,7 +15,7 @@ func BuildFundingGateFromProfile( requiredAmount *moneyv1.Money, ) (*QuoteFundingGate, error) { if profile == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil gate means no funding profile is configured } mode := shared.NormalizeFundingMode(profile.Mode) diff --git a/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_profile_resolver_static.go b/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_profile_resolver_static.go index 62cdfc43..80956845 100644 --- a/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_profile_resolver_static.go +++ b/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_profile_resolver_static.go @@ -89,7 +89,7 @@ func (r *StaticFundingProfileResolver) ResolveGatewayFundingProfile( req FundingProfileRequest, ) (*GatewayFundingProfile, error) { if r == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil resolver means no static funding configuration } gatewayKey := r.gatewayKey(req) @@ -206,7 +206,7 @@ func (r *StaticFundingProfileResolver) ResolveGatewayFundingProfile( } if isEmptyFundingProfile(profile) { - return nil, nil + return nil, nil //nolint:nilnil // nil profile means funding profile is intentionally omitted } return profile, nil } diff --git a/api/payments/quotation/internal/service/quotation/graph_path_finder/service_test.go b/api/payments/quotation/internal/service/quotation/graph_path_finder/service_test.go index c259fb7d..d4034cab 100644 --- a/api/payments/quotation/internal/service/quotation/graph_path_finder/service_test.go +++ b/api/payments/quotation/internal/service/quotation/graph_path_finder/service_test.go @@ -2,10 +2,11 @@ package graph_path_finder import ( "errors" - "github.com/tech/sendico/pkg/discovery" + "slices" "testing" "github.com/tech/sendico/payments/storage/model" + "github.com/tech/sendico/pkg/discovery" "github.com/tech/sendico/pkg/merrors" ) @@ -142,13 +143,5 @@ func railsToStrings(rails []model.Rail) []string { } func equalStrings(got, want []string) bool { - if len(got) != len(want) { - return false - } - for i := range got { - if got[i] != want[i] { - return false - } - } - return true + return slices.Equal(got, want) } diff --git a/api/payments/quotation/internal/service/quotation/helpers.go b/api/payments/quotation/internal/service/quotation/helpers.go index 64d6beb4..9860f864 100644 --- a/api/payments/quotation/internal/service/quotation/helpers.go +++ b/api/payments/quotation/internal/service/quotation/helpers.go @@ -240,7 +240,7 @@ func makeMoney(currency string, value decimal.Decimal) *moneyv1.Money { func ensureCurrency(m *moneyv1.Money, targetCurrency string, quote *oraclev1.Quote) (*moneyv1.Money, error) { if m == nil || strings.TrimSpace(targetCurrency) == "" { - return nil, nil + return nil, nil //nolint:nilnil // nil means no amount available for conversion } if strings.EqualFold(m.GetCurrency(), targetCurrency) { return cloneProtoMoney(m), nil @@ -250,13 +250,13 @@ func ensureCurrency(m *moneyv1.Money, targetCurrency string, quote *oraclev1.Quo func convertWithQuote(m *moneyv1.Money, quote *oraclev1.Quote, targetCurrency string) (*moneyv1.Money, error) { if m == nil || quote == nil || quote.GetPair() == nil || quote.GetPrice() == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil means conversion cannot be performed with available quote } base := strings.TrimSpace(quote.GetPair().GetBase()) qt := strings.TrimSpace(quote.GetPair().GetQuote()) if base == "" || qt == "" || strings.TrimSpace(targetCurrency) == "" { - return nil, nil + return nil, nil //nolint:nilnil // nil means conversion cannot be performed with incomplete pair } price, err := decimal.NewFromString(quote.GetPrice().GetValue()) @@ -274,7 +274,7 @@ func convertWithQuote(m *moneyv1.Money, quote *oraclev1.Quote, targetCurrency st case strings.EqualFold(m.GetCurrency(), qt) && strings.EqualFold(targetCurrency, base): return makeMoney(targetCurrency, value.Div(price)), nil default: - return nil, nil + return nil, nil //nolint:nilnil // nil means quote pair does not match requested conversion } } diff --git a/api/payments/quotation/internal/service/quotation/internal_helpers.go b/api/payments/quotation/internal/service/quotation/internal_helpers.go index 486094bc..2c75c53d 100644 --- a/api/payments/quotation/internal/service/quotation/internal_helpers.go +++ b/api/payments/quotation/internal/service/quotation/internal_helpers.go @@ -14,9 +14,9 @@ import ( func (s *Service) withTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) { if d <= 0 { - return context.WithCancel(ctx) + return context.WithCancel(ctx) //nolint:gosec // cancel func is always invoked by caller } - return context.WithTimeout(ctx, d) + return context.WithTimeout(ctx, d) //nolint:gosec // cancel func is always invoked by caller } func triggerFromKind(kind sharedv1.PaymentKind, requiresFX bool) feesv1.Trigger { diff --git a/api/payments/quotation/internal/service/quotation/quotation_service_v2/service_e2e_test.go b/api/payments/quotation/internal/service/quotation/quotation_service_v2/service_e2e_test.go index 43e21534..18ccc35b 100644 --- a/api/payments/quotation/internal/service/quotation/quotation_service_v2/service_e2e_test.go +++ b/api/payments/quotation/internal/service/quotation/quotation_service_v2/service_e2e_test.go @@ -989,11 +989,11 @@ type staticManagedWalletResolverForE2E struct { func (r staticManagedWalletResolverForE2E) ResolveManagedWalletAsset(_ context.Context, managedWalletRef string) (*paymenttypes.Asset, error) { if len(r.assetsByRef) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // test double: empty map means no managed wallet asset } asset, ok := r.assetsByRef[strings.TrimSpace(managedWalletRef)] if !ok || asset == nil { - return nil, nil + return nil, nil //nolint:nilnil // test double: unknown wallet means no managed wallet asset } cloned := *asset return &cloned, nil @@ -1009,7 +1009,7 @@ func (r staticManagedWalletResolverForE2E) ResolveManagedWalletNetwork(ctx conte func (r staticGatewayRegistryForE2E) List(context.Context) ([]*model.GatewayInstanceDescriptor, error) { if len(r.items) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // test double: empty registry means no gateways configured } out := make([]*model.GatewayInstanceDescriptor, 0, len(r.items)) for _, item := range r.items { diff --git a/api/payments/quotation/internal/service/quotation/quote_computation_service/managed_wallet_network_test.go b/api/payments/quotation/internal/service/quotation/quote_computation_service/managed_wallet_network_test.go index 8e0ee851..89aaa878 100644 --- a/api/payments/quotation/internal/service/quotation/quote_computation_service/managed_wallet_network_test.go +++ b/api/payments/quotation/internal/service/quotation/quote_computation_service/managed_wallet_network_test.go @@ -282,11 +282,11 @@ func (f *fakeManagedWalletNetworkResolver) ResolveManagedWalletAsset(_ context.C return nil, f.assetErr } if f.assets == nil { - return nil, nil + return nil, nil //nolint:nilnil // test double: nil asset map means no resolved asset } src := f.assets[managedWalletRef] if src == nil { - return nil, nil + return nil, nil //nolint:nilnil // test double: missing key means no resolved asset } return &paymenttypes.Asset{ Chain: src.GetChain(), diff --git a/api/payments/quotation/internal/service/quotation/quote_computation_service/planner.go b/api/payments/quotation/internal/service/quotation/quote_computation_service/planner.go index d9010ccf..2561d7b8 100644 --- a/api/payments/quotation/internal/service/quotation/quote_computation_service/planner.go +++ b/api/payments/quotation/internal/service/quotation/quote_computation_service/planner.go @@ -421,7 +421,7 @@ func (s *QuoteComputationService) resolveFundingGate( zap.String("rail", string(in.Rail)), ) - return nil, nil + return nil, nil //nolint:nilnil // nil gate means no resolver configured } s.logger.Debug("Resolving funding gate", @@ -462,7 +462,7 @@ func (s *QuoteComputationService) resolveFundingGate( zap.String("rail", string(in.Rail)), ) - return nil, nil + return nil, nil //nolint:nilnil // nil gate means no funding profile for resolved gateway } gate, err := gateway_funding_profile.BuildFundingGateFromProfile(profile, in.Amount) diff --git a/api/payments/quotation/internal/service/quotation/quote_engine.go b/api/payments/quotation/internal/service/quotation/quote_engine.go index ed01361f..bce41a74 100644 --- a/api/payments/quotation/internal/service/quotation/quote_engine.go +++ b/api/payments/quotation/internal/service/quotation/quote_engine.go @@ -432,13 +432,13 @@ func (s *Service) estimateNetworkFee(ctx context.Context, intent *sharedv1.Payme if err != nil { if errors.Is(err, merrors.ErrNoData) { s.logger.Debug("Network fee estimation skipped: gateway unavailable", zap.Error(err)) - return nil, nil + return nil, nil //nolint:nilnil // nil response means fee estimation skipped when gateway is unavailable } s.logger.Warn("Chain gateway resolution failed", zap.Error(err)) return nil, err } if client == nil { - return nil, nil + return nil, nil //nolint:nilnil // nil response means no chain gateway client is available } resp, err := client.EstimateTransferFee(ctx, req) @@ -457,7 +457,7 @@ func (s *Service) requestFXQuote(ctx context.Context, orgRef string, req *quoteR if fxRequired { return nil, merrors.Internal("fx_oracle_unavailable") } - return nil, nil + return nil, nil //nolint:nilnil // nil quote means FX is optional and oracle is unavailable } meta := req.GetMeta() fxIntent := fxIntentForQuote(intent) @@ -465,7 +465,7 @@ func (s *Service) requestFXQuote(ctx context.Context, orgRef string, req *quoteR if fxRequired { return nil, merrors.InvalidArgument("fx intent missing") } - return nil, nil + return nil, nil //nolint:nilnil // nil quote means FX is not required for this intent } ttl := fxIntent.GetTtlMs() @@ -514,7 +514,7 @@ func (s *Service) requestFXQuote(ctx context.Context, orgRef string, req *quoteR if fxRequired { return nil, merrors.Internal("orchestrator: fx quote missing") } - return nil, nil + return nil, nil //nolint:nilnil // nil quote means FX is optional and oracle returned no quote } return quoteToProto(quote), nil } diff --git a/api/payments/quotation/internal/service/quotation/transfer_intent_hydrator/hydrator.go b/api/payments/quotation/internal/service/quotation/transfer_intent_hydrator/hydrator.go index 177b20b0..f4d97585 100644 --- a/api/payments/quotation/internal/service/quotation/transfer_intent_hydrator/hydrator.go +++ b/api/payments/quotation/internal/service/quotation/transfer_intent_hydrator/hydrator.go @@ -128,10 +128,8 @@ func (h *TransferIntentHydrator) HydrateOne(ctx context.Context, in HydrateOneIn if settlementCurrency == "" { settlementCurrency = settlementCurrencyFromFX(fxIntent) } - requiresFX := false - if fxIntent != nil && fxIntent.Pair != nil { - requiresFX = true - } else { + requiresFX := fxIntent != nil && fxIntent.Pair != nil + if !requiresFX { requiresFX = !strings.EqualFold(amount.Currency, settlementCurrency) } diff --git a/api/payments/storage/.golangci.yml b/api/payments/storage/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/payments/storage/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/pkg/.golangci.yml b/api/pkg/.golangci.yml new file mode 100644 index 00000000..60a80daf --- /dev/null +++ b/api/pkg/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - canonicalheader + - copyloopvar + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - gosec + - govet + - ineffassign + - nilerr + - nilnesserr + - nilnil + - noctx + - rowserrcheck + - sqlclosecheck + - staticcheck + - unconvert + - wastedassign + disable: + - depguard + - exhaustruct + - gochecknoglobals + - gochecknoinits + - gomoddirectives + - wrapcheck + - cyclop + - dupl + - funlen + - gocognit + - gocyclo + - ireturn + - lll + - mnd + - nestif + - nlreturn + - noinlineerr + - paralleltest + - tagliatelle + - testpackage + - varnamelen + - wsl_v5 diff --git a/api/pkg/api/routers/gsresponse/response.go b/api/pkg/api/routers/gsresponse/response.go index f96a45a8..e4ee6e5c 100644 --- a/api/pkg/api/routers/gsresponse/response.go +++ b/api/pkg/api/routers/gsresponse/response.go @@ -38,7 +38,7 @@ func Empty[T any]() Responder[T] { func Error[T any](logger mlogger.Logger, service mservice.Type, code codes.Code, hint string, err error) Responder[T] { return func(ctx context.Context) (*T, error) { fields := []zap.Field{ - zap.String("service", string(service)), + zap.String("service", service), zap.String("status_code", code.String()), } if hint != "" { diff --git a/api/pkg/api/routers/internal/grpcimp/router.go b/api/pkg/api/routers/internal/grpcimp/router.go index 081368a3..e3f5e5b0 100644 --- a/api/pkg/api/routers/internal/grpcimp/router.go +++ b/api/pkg/api/routers/internal/grpcimp/router.go @@ -24,12 +24,12 @@ func (e routerError) Error() string { return string(e) } -type routerErrorWithCause struct { +type routerCauseError struct { message string cause error } -func (e *routerErrorWithCause) Error() string { +func (e *routerCauseError) Error() string { if e == nil { return "" } @@ -39,7 +39,7 @@ func (e *routerErrorWithCause) Error() string { return e.message + ": " + e.cause.Error() } -func (e *routerErrorWithCause) Unwrap() error { +func (e *routerCauseError) Unwrap() error { if e == nil { return nil } @@ -47,7 +47,7 @@ func (e *routerErrorWithCause) Unwrap() error { } func newRouterErrorWithCause(message string, cause error) error { - return &routerErrorWithCause{ + return &routerCauseError{ message: message, cause: cause, } @@ -104,7 +104,7 @@ func NewRouter(logger mlogger.Logger, cfg *Config, opts *Options) (*Router, erro listener := opts.Listener var err error if listener == nil { - listener, err = net.Listen(network, address) + listener, err = (&net.ListenConfig{}).Listen(context.Background(), network, address) if err != nil { return nil, newRouterErrorWithCause(errMsgListenFailed, err) } @@ -120,9 +120,11 @@ func NewRouter(logger mlogger.Logger, cfg *Config, opts *Options) (*Router, erro serverOpts = append(serverOpts, grpc.MaxSendMsgSize(cfg.MaxSendMsgSize)) } - if creds, err := configureTLS(cfg.TLS); err != nil { - return nil, err - } else if creds != nil { + if cfg.TLS != nil { + creds, err := configureTLS(cfg.TLS) + if err != nil { + return nil, err + } serverOpts = append(serverOpts, grpc.Creds(creds)) } @@ -253,10 +255,6 @@ func (r *Router) Done() <-chan error { } func configureTLS(cfg *TLSConfig) (credentials.TransportCredentials, error) { - if cfg == nil { - return nil, nil - } - if cfg.CertFile == "" || cfg.KeyFile == "" { return nil, errTLSMissingCertAndKey } diff --git a/api/pkg/api/routers/internal/grpcimp/router_test.go b/api/pkg/api/routers/internal/grpcimp/router_test.go index a7a3e075..e87f3e89 100644 --- a/api/pkg/api/routers/internal/grpcimp/router_test.go +++ b/api/pkg/api/routers/internal/grpcimp/router_test.go @@ -18,7 +18,7 @@ func newBufferedListener(t *testing.T) *bufconn.Listener { listener := bufconn.Listen(bufconnSize) t.Cleanup(func() { - listener.Close() + require.NoError(t, listener.Close()) }) return listener diff --git a/api/pkg/auth/dbimp.go b/api/pkg/auth/dbimp.go index de6e3b20..24e13dc4 100644 --- a/api/pkg/auth/dbimp.go +++ b/api/pkg/auth/dbimp.go @@ -48,7 +48,7 @@ func (db *ProtectedDBImp[T]) enforce(ctx context.Context, action model.Action, o func (db *ProtectedDBImp[T]) Create(ctx context.Context, accountRef, organizationRef bson.ObjectID, object T) error { db.DBImp.Logger.Debug("Attempting to create object", mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", db.Collection)) if object.GetPermissionRef() == bson.NilObjectID { object.SetPermissionRef(db.PermissionRef) @@ -61,12 +61,12 @@ func (db *ProtectedDBImp[T]) Create(ctx context.Context, accountRef, organizatio if err := db.DBImp.Create(ctx, object); err != nil { db.DBImp.Logger.Warn("Failed to create object", zap.Error(err), mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", db.Collection)) return err } db.DBImp.Logger.Debug("Successfully created object", mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", db.Collection)) return nil } @@ -76,7 +76,7 @@ func (db *ProtectedDBImp[T]) InsertMany(ctx context.Context, accountRef, organiz } db.DBImp.Logger.Debug("Attempting to insert many objects", mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)), + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", db.Collection), zap.Int("count", len(objects))) // Set permission and organization refs for all objects and enforce permissions @@ -93,13 +93,13 @@ func (db *ProtectedDBImp[T]) InsertMany(ctx context.Context, accountRef, organiz if err := db.DBImp.InsertMany(ctx, objects); err != nil { db.DBImp.Logger.Warn("Failed to insert many objects", zap.Error(err), mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)), + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", db.Collection), zap.Int("count", len(objects))) return err } db.DBImp.Logger.Debug("Successfully inserted many objects", mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)), + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", db.Collection), zap.Int("count", len(objects))) return nil } @@ -127,7 +127,7 @@ func (db *ProtectedDBImp[T]) Get(ctx context.Context, accountRef, objectRef bson if err := db.DBImp.Get(ctx, objectRef, result); err != nil { db.DBImp.Logger.Warn("Failed to get object", zap.Error(err), mzap.AccRef(accountRef), - mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("object_ref", objectRef), zap.String("collection", db.Collection)) return err } @@ -182,18 +182,18 @@ func (db *ProtectedDBImp[T]) ListIDs( query builder.Query, ) ([]bson.ObjectID, error) { db.DBImp.Logger.Debug("Attempting to list object IDs", - mzap.AccRef(accountRef), zap.String("collection", string(db.Collection)), zap.Any("filter", query.BuildQuery())) + mzap.AccRef(accountRef), zap.String("collection", db.Collection), zap.Any("filter", query.BuildQuery())) // 1. Fetch all candidate IDs from the underlying DB allIDs, err := db.DBImp.ListPermissionBound(ctx, query) if err != nil { db.DBImp.Logger.Warn("Failed to list object IDs", zap.Error(err), mzap.AccRef(accountRef), - zap.String("collection", string(db.Collection)), zap.String("action", string(action))) + zap.String("collection", db.Collection), zap.String("action", string(action))) return nil, err } if len(allIDs) == 0 { db.DBImp.Logger.Debug("No objects found matching filter", mzap.AccRef(accountRef), - zap.String("collection", string(db.Collection)), zap.Any("filter", query.BuildQuery())) + zap.String("collection", db.Collection), zap.Any("filter", query.BuildQuery())) return []bson.ObjectID{}, merrors.NoData(fmt.Sprintf("no %s found", db.Collection)) } @@ -203,12 +203,12 @@ func (db *ProtectedDBImp[T]) ListIDs( enforceErr := db.enforce(ctx, action, desc, accountRef, *desc.GetID()) if enforceErr == nil { allowedIDs = append(allowedIDs, *desc.GetID()) - } else if !errors.Is(err, merrors.ErrAccessDenied) { + } else if !errors.Is(enforceErr, merrors.ErrAccessDenied) { // If the error is something other than AccessDenied, we want to fail db.DBImp.Logger.Warn("Error while enforcing read permission", zap.Error(enforceErr), mzap.ObjRef("permission_ref", desc.GetPermissionRef()), zap.String("action", string(action)), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", desc.GetOrganizationRef()), - mzap.ObjRef("object_ref", *desc.GetID()), zap.String("collection", string(db.Collection)), + mzap.ObjRef("object_ref", *desc.GetID()), zap.String("collection", db.Collection), ) return nil, enforceErr } @@ -217,7 +217,7 @@ func (db *ProtectedDBImp[T]) ListIDs( db.DBImp.Logger.Debug("Successfully enforced read permission on IDs", zap.Int("fetched_count", len(allIDs)), zap.Int("allowed_count", len(allowedIDs)), mzap.AccRef(accountRef), - zap.String("collection", string(db.Collection)), zap.String("action", string(action))) + zap.String("collection", db.Collection), zap.String("action", string(action))) // 3. Return only the IDs that passed permission checks return allowedIDs, nil @@ -249,7 +249,7 @@ func CreateDBImp[T model.PermissionBoundStorable]( logger := l.Named("protected") var policy model.PolicyDescription if err := pdb.GetBuiltInPolicy(ctx, collection, &policy); err != nil { - logger.Warn("Failed to fetch policy description", zap.Error(err), zap.String("resource_type", string(collection))) + logger.Warn("Failed to fetch policy description", zap.Error(err), zap.String("resource_type", collection)) return nil, err } p := &ProtectedDBImp[T]{ @@ -261,7 +261,7 @@ func CreateDBImp[T model.PermissionBoundStorable]( if err := p.DBImp.Repository.CreateIndex(&ri.Definition{ Keys: []ri.Key{{Field: storable.OrganizationRefField, Sort: ri.Asc}}, }); err != nil { - logger.Warn("Failed to create index", zap.Error(err), zap.String("resource_type", string(collection))) + logger.Warn("Failed to create index", zap.Error(err), zap.String("resource_type", collection)) return nil, err } diff --git a/api/pkg/auth/dbimpab.go b/api/pkg/auth/dbimpab.go index 54139373..7c410fe0 100644 --- a/api/pkg/auth/dbimpab.go +++ b/api/pkg/auth/dbimpab.go @@ -89,7 +89,7 @@ func (db *AccountBoundDBImp[T]) enforceInterface(ctx context.Context, action mod func (db *AccountBoundDBImp[T]) Create(ctx context.Context, accountRef bson.ObjectID, object T) error { orgRef := object.GetOrganizationRef() db.Logger.Debug("Attempting to create object", mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("organization_ref", orgRef), zap.String("collection", db.Collection)) // Check organization update permission for create operations if err := db.enforce(ctx, model.ActionUpdate, object, accountRef); err != nil { @@ -98,12 +98,12 @@ func (db *AccountBoundDBImp[T]) Create(ctx context.Context, accountRef bson.Obje if err := db.DBImp.Create(ctx, object); err != nil { db.Logger.Warn("Failed to create object", zap.Error(err), mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("organization_ref", orgRef), zap.String("collection", db.Collection)) return err } db.Logger.Debug("Successfully created object", mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("organization_ref", orgRef), zap.String("collection", db.Collection)) return nil } @@ -113,7 +113,7 @@ func (db *AccountBoundDBImp[T]) Get(ctx context.Context, accountRef, objectRef b // First get the object to check its organization if err := db.DBImp.Get(ctx, objectRef, result); err != nil { db.Logger.Warn("Failed to get object", zap.Error(err), mzap.AccRef(accountRef), - mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("object_ref", objectRef), zap.String("collection", db.Collection)) return err } @@ -123,7 +123,7 @@ func (db *AccountBoundDBImp[T]) Get(ctx context.Context, accountRef, objectRef b } db.Logger.Debug("Successfully retrieved object", mzap.AccRef(accountRef), - mzap.ObjRef("organization_ref", result.GetOrganizationRef()), zap.String("collection", string(db.Collection))) + mzap.ObjRef("organization_ref", result.GetOrganizationRef()), zap.String("collection", db.Collection)) return nil } @@ -167,7 +167,7 @@ func (db *AccountBoundDBImp[T]) Patch(ctx context.Context, accountRef, objectRef if err := db.DBImp.Patch(ctx, objectRef, patch); err != nil { db.Logger.Warn("Failed to patch object", zap.Error(err), mzap.AccRef(accountRef), - mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("object_ref", objectRef), zap.String("collection", db.Collection)) return err } @@ -195,7 +195,7 @@ func (db *AccountBoundDBImp[T]) Delete(ctx context.Context, accountRef, objectRe if err := db.DBImp.Delete(ctx, objectRef); err != nil { db.Logger.Warn("Failed to delete object", zap.Error(err), mzap.AccRef(accountRef), - mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) + mzap.ObjRef("object_ref", objectRef), zap.String("collection", db.Collection)) return err } @@ -204,7 +204,7 @@ func (db *AccountBoundDBImp[T]) Delete(ctx context.Context, accountRef, objectRe } func (db *AccountBoundDBImp[T]) DeleteMany(ctx context.Context, accountRef bson.ObjectID, query builder.Query) error { - db.Logger.Debug("Attempting to delete many objects", mzap.AccRef(accountRef), zap.String("collection", string(db.Collection))) + db.Logger.Debug("Attempting to delete many objects", mzap.AccRef(accountRef), zap.String("collection", db.Collection)) // Get all candidate objects for batch permission checking allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query) @@ -245,7 +245,7 @@ func (db *AccountBoundDBImp[T]) DeleteMany(ctx context.Context, accountRef bson. } func (db *AccountBoundDBImp[T]) FindOne(ctx context.Context, accountRef bson.ObjectID, query builder.Query, result T) error { - db.Logger.Debug("Attempting to find one object", mzap.AccRef(accountRef), zap.String("collection", string(db.Collection))) + db.Logger.Debug("Attempting to find one object", mzap.AccRef(accountRef), zap.String("collection", db.Collection)) // For FindOne, we need to check read permission after finding the object if err := db.DBImp.FindOne(ctx, query, result); err != nil { @@ -264,7 +264,7 @@ func (db *AccountBoundDBImp[T]) FindOne(ctx context.Context, accountRef bson.Obj } func (db *AccountBoundDBImp[T]) ListIDs(ctx context.Context, accountRef bson.ObjectID, query builder.Query) ([]bson.ObjectID, error) { - db.Logger.Debug("Attempting to list object IDs", mzap.AccRef(accountRef), zap.String("collection", string(db.Collection))) + db.Logger.Debug("Attempting to list object IDs", mzap.AccRef(accountRef), zap.String("collection", db.Collection)) // Get all candidate objects for batch permission checking allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query) @@ -294,7 +294,7 @@ func (db *AccountBoundDBImp[T]) ListIDs(ctx context.Context, accountRef bson.Obj } func (db *AccountBoundDBImp[T]) ListAccountBound(ctx context.Context, accountRef, organizationRef bson.ObjectID, query builder.Query) ([]model.AccountBoundStorable, error) { - db.Logger.Debug("Attempting to list account bound objects", mzap.AccRef(accountRef), zap.String("collection", string(db.Collection))) + db.Logger.Debug("Attempting to list account bound objects", mzap.AccRef(accountRef), zap.String("collection", db.Collection)) // Build query to find objects where accountRef matches OR is null/absent accountQuery := repository.WithOrg(accountRef, organizationRef) diff --git a/api/pkg/auth/dbimpab_test.go b/api/pkg/auth/dbimpab_test.go index 711058b2..20b7190d 100644 --- a/api/pkg/auth/dbimpab_test.go +++ b/api/pkg/auth/dbimpab_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/tech/sendico/pkg/merrors" - "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" @@ -14,7 +13,7 @@ import ( // TestAccountBoundDBImp_Enforce tests the enforce method func TestAccountBoundDBImp_Enforce(t *testing.T) { - logger := mlogger.Logger(zap.NewNop()) + logger := zap.NewNop() db := &AccountBoundDBImp[model.AccountBoundStorable]{ Logger: logger, PermissionRef: bson.NewObjectID(), @@ -34,13 +33,13 @@ func TestAccountBoundDBImp_Enforce(t *testing.T) { t.Run("CollectionSet", func(t *testing.T) { // Test that Collection is properly set - assert.Equal(t, "test_collection", string(db.Collection)) + assert.Equal(t, "test_collection", db.Collection) }) } // TestAccountBoundDBImp_InterfaceCompliance tests that the struct implements required interfaces func TestAccountBoundDBImp_InterfaceCompliance(t *testing.T) { - logger := mlogger.Logger(zap.NewNop()) + logger := zap.NewNop() db := &AccountBoundDBImp[model.AccountBoundStorable]{ Logger: logger, PermissionRef: bson.NewObjectID(), diff --git a/api/pkg/auth/internal/casbin/action.go b/api/pkg/auth/internal/casbin/action.go deleted file mode 100644 index e19efb10..00000000 --- a/api/pkg/auth/internal/casbin/action.go +++ /dev/null @@ -1,23 +0,0 @@ -package casbin - -import ( - "fmt" - - "github.com/tech/sendico/pkg/merrors" - "github.com/tech/sendico/pkg/model" -) - -func stringToAction(actionStr string) (model.Action, error) { - switch actionStr { - case string(model.ActionCreate): - return model.ActionCreate, nil - case string(model.ActionRead): - return model.ActionRead, nil - case string(model.ActionUpdate): - return model.ActionUpdate, nil - case string(model.ActionDelete): - return model.ActionDelete, nil - default: - return "", merrors.InvalidArgument(fmt.Sprintf("invalid action: %s", actionStr), "action") - } -} diff --git a/api/pkg/auth/internal/casbin/config/config.go b/api/pkg/auth/internal/casbin/config/config.go index c28bed4c..5c0356f6 100644 --- a/api/pkg/auth/internal/casbin/config/config.go +++ b/api/pkg/auth/internal/casbin/config/config.go @@ -80,9 +80,10 @@ func getEnvBoolValue(logger mlogger.Logger, varName, envVarName string, value *b if envValue != nil { envStr := os.Getenv(*envValue) - if envStr == "true" || envStr == "1" { + switch envStr { + case "true", "1": return true - } else if envStr == "false" || envStr == "0" { + case "false", "0": return false } logger.Warn("Invalid environment variable value for boolean", zap.String("environment_variable", envVarName), zap.String("value", envStr)) diff --git a/api/pkg/auth/internal/native/db/policies.go b/api/pkg/auth/internal/native/db/policies.go index 00d1221c..51f51ec2 100644 --- a/api/pkg/auth/internal/native/db/policies.go +++ b/api/pkg/auth/internal/native/db/policies.go @@ -116,7 +116,7 @@ func NewPoliciesDB(logger mlogger.Logger, db *mongo.Database) (*PermissionsDBImp {Field: "policy.objectRef", Sort: ri.Asc}, }, } - if err := p.DBImp.Repository.CreateIndex(policiesQueryIndex); err != nil { + if err := p.Repository.CreateIndex(policiesQueryIndex); err != nil { p.Logger.Warn("Failed to prepare policies query index", zap.Error(err)) return nil, err } @@ -127,7 +127,7 @@ func NewPoliciesDB(logger mlogger.Logger, db *mongo.Database) (*PermissionsDBImp {Field: "policy.effect.action", Sort: ri.Asc}, }, } - if err := p.DBImp.Repository.CreateIndex(roleBasedQueriesIndex); err != nil { + if err := p.Repository.CreateIndex(roleBasedQueriesIndex); err != nil { p.Logger.Warn("Failed to prepare role based query index", zap.Error(err)) return nil, err } @@ -142,7 +142,7 @@ func NewPoliciesDB(logger mlogger.Logger, db *mongo.Database) (*PermissionsDBImp }, Unique: true, } - if err := p.DBImp.Repository.CreateIndex(uniquePolicyConstaint); err != nil { + if err := p.Repository.CreateIndex(uniquePolicyConstaint); err != nil { p.Logger.Warn("Failed to unique policy assignment index", zap.Error(err)) return nil, err } diff --git a/api/pkg/auth/internal/native/db/roles.go b/api/pkg/auth/internal/native/db/roles.go index 9019b769..b5786804 100644 --- a/api/pkg/auth/internal/native/db/roles.go +++ b/api/pkg/auth/internal/native/db/roles.go @@ -68,14 +68,14 @@ func NewRolesDB(logger mlogger.Logger, db *mongo.Database) (*RolesDBImp, error) DBImp: *template.Create[*nstructures.RoleAssignment](logger, "role_assignments", db), } - if err := p.DBImp.Repository.CreateIndex(&ri.Definition{ + if err := p.Repository.CreateIndex(&ri.Definition{ Keys: []ri.Key{{Field: "role.organizationRef", Sort: ri.Asc}}, }); err != nil { p.Logger.Warn("Failed to prepare venue index", zap.Error(err)) return nil, err } - if err := p.DBImp.Repository.CreateIndex(&ri.Definition{ + if err := p.Repository.CreateIndex(&ri.Definition{ Keys: []ri.Key{{Field: "role.descriptionRef", Sort: ri.Asc}}, }); err != nil { p.Logger.Warn("Failed to prepare role description index", zap.Error(err)) @@ -90,7 +90,7 @@ func NewRolesDB(logger mlogger.Logger, db *mongo.Database) (*RolesDBImp, error) }, Unique: true, } - if err := p.DBImp.Repository.CreateIndex(uniqueRoleConstaint); err != nil { + if err := p.Repository.CreateIndex(uniqueRoleConstaint); err != nil { p.Logger.Warn("Failed to prepare role assignment index", zap.Error(err)) return nil, err } diff --git a/api/pkg/auth/internal/native/enforcer_test.go b/api/pkg/auth/internal/native/enforcer_test.go index cd7bb03c..ea6e6112 100644 --- a/api/pkg/auth/internal/native/enforcer_test.go +++ b/api/pkg/auth/internal/native/enforcer_test.go @@ -215,14 +215,14 @@ func createTestRoleAssignment(roleRef, accountRef, organizationRef bson.ObjectID } } -func createTestPolicyAssignment(roleRef bson.ObjectID, action model.Action, effect model.Effect, organizationRef, descriptionRef bson.ObjectID, objectRef *bson.ObjectID) nstructures.PolicyAssignment { +func createTestPolicyAssignment(roleRef bson.ObjectID, effect model.Effect, organizationRef, descriptionRef bson.ObjectID, objectRef *bson.ObjectID) nstructures.PolicyAssignment { return nstructures.PolicyAssignment{ Policy: model.Policy{ OrganizationRef: organizationRef, DescriptionRef: descriptionRef, ObjectRef: objectRef, Effect: model.ActionEffect{ - Action: action, + Action: model.ActionRead, Effect: effect, }, }, @@ -259,7 +259,7 @@ func TestEnforcer_Enforce(t *testing.T) { mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) // Mock policy assignment with ALLOW effect - policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, &objectRef) + policyAssignment := createTestPolicyAssignment(roleRef, model.EffectAllow, organizationRef, permissionRef, &objectRef) mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{policyAssignment}, nil) // Create enforcer @@ -284,7 +284,7 @@ func TestEnforcer_Enforce(t *testing.T) { mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) // Mock policy assignment with DENY effect - policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectDeny, organizationRef, permissionRef, &objectRef) + policyAssignment := createTestPolicyAssignment(roleRef, model.EffectDeny, organizationRef, permissionRef, &objectRef) mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{policyAssignment}, nil) enforcer := createTestEnforcer(mockPDB, mockRDB) @@ -312,11 +312,11 @@ func TestEnforcer_Enforce(t *testing.T) { mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment1, roleAssignment2}, nil) // First role has ALLOW policy - allowPolicy := createTestPolicyAssignment(role1Ref, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, &objectRef) + allowPolicy := createTestPolicyAssignment(role1Ref, model.EffectAllow, organizationRef, permissionRef, &objectRef) mockPDB.On("PoliciesForPermissionAction", ctx, role1Ref, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{allowPolicy}, nil) // Second role has DENY policy - should take precedence - denyPolicy := createTestPolicyAssignment(role2Ref, model.ActionRead, model.EffectDeny, organizationRef, permissionRef, &objectRef) + denyPolicy := createTestPolicyAssignment(role2Ref, model.EffectDeny, organizationRef, permissionRef, &objectRef) mockPDB.On("PoliciesForPermissionAction", ctx, role2Ref, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{denyPolicy}, nil) enforcer := createTestEnforcer(mockPDB, mockRDB) @@ -445,7 +445,7 @@ func TestEnforcer_Enforce(t *testing.T) { mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) // Mock corrupted policy with invalid effect - corruptedPolicy := createTestPolicyAssignment(roleRef, model.ActionRead, "invalid_effect", organizationRef, permissionRef, &objectRef) + corruptedPolicy := createTestPolicyAssignment(roleRef, "invalid_effect", organizationRef, permissionRef, &objectRef) mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{corruptedPolicy}, nil) enforcer := createTestEnforcer(mockPDB, mockRDB) @@ -539,7 +539,7 @@ func TestEnforcer_EnforceBatch(t *testing.T) { mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) // Mock policy assignment with ALLOW effect - policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, nil) + policyAssignment := createTestPolicyAssignment(roleRef, model.EffectAllow, organizationRef, permissionRef, nil) mockPDB.On("PoliciesForRoles", ctx, []bson.ObjectID{roleRef}, model.ActionRead).Return([]nstructures.PolicyAssignment{policyAssignment}, nil) enforcer := createTestEnforcer(mockPDB, mockRDB) @@ -664,7 +664,7 @@ func TestEnforcer_GetPermissions(t *testing.T) { mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) // Mock policy assignment - policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, createTestObjectID(), nil) + policyAssignment := createTestPolicyAssignment(roleRef, model.EffectAllow, organizationRef, createTestObjectID(), nil) mockPDB.On("PoliciesForRole", ctx, roleRef).Return([]nstructures.PolicyAssignment{policyAssignment}, nil) enforcer := createTestEnforcer(mockPDB, mockRDB) @@ -702,8 +702,8 @@ func TestEnforcer_SecurityScenarios(t *testing.T) { mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) // Mock multiple policies: both ALLOW and DENY - allowPolicy := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, &objectRef) - denyPolicy := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectDeny, organizationRef, permissionRef, &objectRef) + allowPolicy := createTestPolicyAssignment(roleRef, model.EffectAllow, organizationRef, permissionRef, &objectRef) + denyPolicy := createTestPolicyAssignment(roleRef, model.EffectDeny, organizationRef, permissionRef, &objectRef) mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{allowPolicy, denyPolicy}, nil) enforcer := createTestEnforcer(mockPDB, mockRDB) diff --git a/api/pkg/db/connection.go b/api/pkg/db/connection.go index a9874817..ce891468 100644 --- a/api/pkg/db/connection.go +++ b/api/pkg/db/connection.go @@ -31,16 +31,10 @@ func (c *MongoConnection) Database() *mongo.Database { } func (c *MongoConnection) Disconnect(ctx context.Context) error { - if ctx == nil { - ctx = context.Background() - } return c.client.Disconnect(ctx) } func (c *MongoConnection) Ping(ctx context.Context) error { - if ctx == nil { - ctx = context.Background() - } return c.client.Ping(ctx, readpref.Primary()) } diff --git a/api/pkg/db/internal/mongo/accountdb/db.go b/api/pkg/db/internal/mongo/accountdb/db.go index d5136bc4..806e0607 100644 --- a/api/pkg/db/internal/mongo/accountdb/db.go +++ b/api/pkg/db/internal/mongo/accountdb/db.go @@ -19,7 +19,7 @@ func Create(logger mlogger.Logger, db *mongo.Database) (*AccountDB, error) { DBImp: *template.Create[*model.Account](logger, mservice.Accounts, db), } - if err := p.DBImp.Repository.CreateIndex(&ri.Definition{ + if err := p.Repository.CreateIndex(&ri.Definition{ Keys: []ri.Key{{Field: "login", Sort: ri.Asc}}, Unique: true, }); err != nil { diff --git a/api/pkg/db/internal/mongo/chainassetsdb/resolve.go b/api/pkg/db/internal/mongo/chainassetsdb/resolve.go index 79f5606c..3f6ca06c 100644 --- a/api/pkg/db/internal/mongo/chainassetsdb/resolve.go +++ b/api/pkg/db/internal/mongo/chainassetsdb/resolve.go @@ -14,5 +14,5 @@ func (db *ChainAssetsDB) Resolve(ctx context.Context, chainAsset model.ChainAsse repository.Query().Filter(assetField.Dot("chain"), chainAsset.Chain), repository.Query().Filter(assetField.Dot("tokenSymbol"), chainAsset.TokenSymbol), ) - return &assetDescription, db.DBImp.FindOne(ctx, q, &assetDescription) + return &assetDescription, db.FindOne(ctx, q, &assetDescription) } diff --git a/api/pkg/db/internal/mongo/repositoryimp/builderimp/func.go b/api/pkg/db/internal/mongo/repositoryimp/builderimp/func.go index d2516994..b69410e9 100644 --- a/api/pkg/db/internal/mongo/repositoryimp/builderimp/func.go +++ b/api/pkg/db/internal/mongo/repositoryimp/builderimp/func.go @@ -81,7 +81,7 @@ type computeImp struct { func (a *computeImp) Build() any { return bson.D{ - {Key: string(a.field.Build()), Value: a.expression.Build()}, + {Key: a.field.Build(), Value: a.expression.Build()}, } } diff --git a/api/pkg/db/internal/mongo/repositoryimp/repository.go b/api/pkg/db/internal/mongo/repositoryimp/repository.go index eba2470d..acebfc7a 100644 --- a/api/pkg/db/internal/mongo/repositoryimp/repository.go +++ b/api/pkg/db/internal/mongo/repositoryimp/repository.go @@ -101,7 +101,9 @@ func (r *MongoRepository) executeQuery(ctx context.Context, queryFunc QueryFunc, if err != nil { return err } - defer cursor.Close(ctx) + defer func() { + _ = cursor.Close(ctx) + }() for cursor.Next(ctx) { if err = decoder(cursor); err != nil { @@ -165,7 +167,9 @@ func (r *MongoRepository) ListIDs(ctx context.Context, query builder.Query) ([]b if err != nil { return nil, err } - defer cursor.Close(ctx) + defer func() { + _ = cursor.Close(ctx) + }() var ids []bson.ObjectID for cursor.Next(ctx) { @@ -196,7 +200,9 @@ func (r *MongoRepository) ListPermissionBound(ctx context.Context, query builder if err != nil { return nil, err } - defer cursor.Close(ctx) + defer func() { + _ = cursor.Close(ctx) + }() result := make([]model.PermissionBoundStorable, 0) @@ -226,7 +232,9 @@ func (r *MongoRepository) ListAccountBound(ctx context.Context, query builder.Qu if err != nil { return nil, err } - defer cursor.Close(ctx) + defer func() { + _ = cursor.Close(ctx) + }() result := make([]model.AccountBoundStorable, 0) diff --git a/api/pkg/db/internal/mongo/tseriesimp/tseries.go b/api/pkg/db/internal/mongo/tseriesimp/tseries.go index e30ef135..8f96fe69 100644 --- a/api/pkg/db/internal/mongo/tseriesimp/tseries.go +++ b/api/pkg/db/internal/mongo/tseriesimp/tseries.go @@ -45,7 +45,8 @@ func NewMongoTimeSeriesCollection(ctx context.Context, db *mongo.Database, tsOpt } if err := db.CreateCollection(ctx, tsOpts.Collection, collOpts); err != nil { - if cmdErr, ok := err.(mongo.CommandError); !ok || cmdErr.Code != 48 { + var cmdErr mongo.CommandError + if !errors.As(err, &cmdErr) || cmdErr.Code != 48 { return nil, err } } @@ -86,7 +87,9 @@ func (ts *TimeSeries) executeQuery(ctx context.Context, decoder rdecoder.Decodin if err != nil { return err } - defer cursor.Close(ctx) + defer func() { + _ = cursor.Close(ctx) + }() for cursor.Next(ctx) { if err := cursor.Err(); err != nil { diff --git a/api/pkg/db/internal/mongo/verificationimp/consume.go b/api/pkg/db/internal/mongo/verificationimp/consume.go index 34ed97be..3063eb1a 100644 --- a/api/pkg/db/internal/mongo/verificationimp/consume.go +++ b/api/pkg/db/internal/mongo/verificationimp/consume.go @@ -71,7 +71,7 @@ func (db *verificationDB) Consume( zap.String("account_ref", accountRefHex), ) var direct model.VerificationToken - err := db.DBImp.FindOne(ctx, magicFilter, &direct) + err := db.FindOne(ctx, magicFilter, &direct) switch { case err == nil: token = &direct @@ -118,7 +118,7 @@ func (db *verificationDB) Consume( zap.Any("scope_filter", scopeFilter.BuildQuery()), ) tokens, err := mutil.GetObjects[model.VerificationToken]( - ctx, db.Logger, scopeFilter, nil, db.DBImp.Repository, + ctx, db.Logger, scopeFilter, nil, db.Repository, ) if err != nil { if errors.Is(err, merrors.ErrNoData) { @@ -182,7 +182,7 @@ func (db *verificationDB) Consume( zap.String("account_ref", accountRefHex), ) - incremented, patchErr := db.DBImp.PatchMany( + incremented, patchErr := db.PatchMany( ctx, activeFilter, repository.Patch().Inc(repository.Field("attempts"), 1), @@ -272,7 +272,7 @@ func (db *verificationDB) Consume( mzap.StorableRef(token), ) - updated, err := db.DBImp.PatchMany( + updated, err := db.PatchMany( ctx, consumeFilter, repository.Patch().Set(repository.Field("usedAt"), now), @@ -309,7 +309,7 @@ func (db *verificationDB) Consume( } // 5) Consume failed → increment attempts - incremented, incrementErr := db.DBImp.PatchMany( + incremented, incrementErr := db.PatchMany( ctx, repository.IDFilter(token.ID), repository.Patch().Inc(repository.Field("attempts"), 1), @@ -335,7 +335,7 @@ func (db *verificationDB) Consume( // 6) Re-check state var fresh model.VerificationToken - if err := db.DBImp.FindOne(ctx, repository.IDFilter(token.ID), &fresh); err != nil { + if err := db.FindOne(ctx, repository.IDFilter(token.ID), &fresh); err != nil { db.Logger.Warn("Verification consume failed to re-check token state", zap.String("purpose", string(purpose)), zap.Bool("account_scoped", accountScoped), diff --git a/api/pkg/db/internal/mongo/verificationimp/create.go b/api/pkg/db/internal/mongo/verificationimp/create.go index 45edea59..08aef87b 100644 --- a/api/pkg/db/internal/mongo/verificationimp/create.go +++ b/api/pkg/db/internal/mongo/verificationimp/create.go @@ -166,22 +166,22 @@ func (db *verificationDB) Create( // Optional idempotency key support for safe retries. if hasIdempotency { var sameToken model.VerificationToken - err := db.DBImp.FindOne(tx, hashFilter(token.VerifyTokenHash), &sameToken) + err := db.FindOne(tx, hashFilter(token.VerifyTokenHash), &sameToken) switch { case err == nil: // Same hash means the same Create operation already succeeded. - return nil, nil + return struct{}{}, nil case errors.Is(err, merrors.ErrNoData): default: return nil, err } var existing model.VerificationToken - err = db.DBImp.FindOne(tx, idempotencyFilter(request, idempotencyKey), &existing) + err = db.FindOne(tx, idempotencyFilter(request, idempotencyKey), &existing) switch { case err == nil: // Existing request with the same idempotency scope has already succeeded. - return nil, nil + return struct{}{}, nil case errors.Is(err, merrors.ErrNoData): default: return nil, err @@ -193,7 +193,7 @@ func (db *verificationDB) Create( cutoff := now.Add(-*request.Cooldown) var recent model.VerificationToken - err := db.DBImp.FindOne(tx, cooldownActiveContextFilter(request, now, cutoff), &recent) + err := db.FindOne(tx, cooldownActiveContextFilter(request, now, cutoff), &recent) switch { case err == nil: return nil, verification.ErrorCooldownActive() @@ -204,7 +204,7 @@ func (db *verificationDB) Create( } // 2) Invalidate active tokens for this context - if _, err := db.DBImp.PatchMany( + if _, err := db.PatchMany( tx, activeFilter, repository.Patch().Set(repository.Field("usedAt"), now), @@ -216,20 +216,20 @@ func (db *verificationDB) Create( if err := db.DBImp.Create(tx, token); err != nil { if hasIdempotency && errors.Is(err, merrors.ErrDataConflict) { var sameToken model.VerificationToken - findErr := db.DBImp.FindOne(tx, hashFilter(token.VerifyTokenHash), &sameToken) + findErr := db.FindOne(tx, hashFilter(token.VerifyTokenHash), &sameToken) switch { case findErr == nil: - return nil, nil + return struct{}{}, nil case errors.Is(findErr, merrors.ErrNoData): default: return nil, findErr } var existing model.VerificationToken - findErr = db.DBImp.FindOne(tx, idempotencyFilter(request, idempotencyKey), &existing) + findErr = db.FindOne(tx, idempotencyFilter(request, idempotencyKey), &existing) switch { case findErr == nil: - return nil, nil + return struct{}{}, nil case errors.Is(findErr, merrors.ErrNoData): default: return nil, findErr @@ -237,7 +237,7 @@ func (db *verificationDB) Create( } return nil, err } - return nil, nil + return struct{}{}, nil }) if err != nil { diff --git a/api/pkg/db/internal/mongo/verificationimp/verification_test.go b/api/pkg/db/internal/mongo/verificationimp/verification_test.go index 9220730a..95236d38 100644 --- a/api/pkg/db/internal/mongo/verificationimp/verification_test.go +++ b/api/pkg/db/internal/mongo/verificationimp/verification_test.go @@ -211,7 +211,7 @@ func (m *memoryTokenRepository) InsertMany(ctx context.Context, objs []storable. } return nil } -func (m *memoryTokenRepository) FindManyByFilter(_ context.Context, query builder.Query, decoder rd.DecodingFunc) error { +func (m *memoryTokenRepository) FindManyByFilter(ctx context.Context, query builder.Query, decoder rd.DecodingFunc) error { m.mu.Lock() var matches []interface{} for _, id := range m.order { @@ -231,9 +231,11 @@ func (m *memoryTokenRepository) FindManyByFilter(_ context.Context, query builde if err != nil { return err } - defer cur.Close(context.Background()) + defer func() { + _ = cur.Close(ctx) + }() - for cur.Next(context.Background()) { + for cur.Next(ctx) { if err := decoder(cur); err != nil { return err } diff --git a/api/pkg/discovery/messages.go b/api/pkg/discovery/messages.go index e062737e..a94207ee 100644 --- a/api/pkg/discovery/messages.go +++ b/api/pkg/discovery/messages.go @@ -3,8 +3,8 @@ package discovery import ( "encoding/json" - messaging "github.com/tech/sendico/pkg/messaging/envelope" "github.com/tech/sendico/pkg/merrors" + messaging "github.com/tech/sendico/pkg/messaging/envelope" "github.com/tech/sendico/pkg/model" ) @@ -21,7 +21,7 @@ func (e *jsonEnvelope) Serialize() ([]byte, error) { if err != nil { return nil, err } - return e.Envelope.Wrap(data) + return e.Wrap(data) } func newEnvelope(sender string, event model.NotificationEvent, payload any) messaging.Envelope { diff --git a/api/pkg/discovery/service.go b/api/pkg/discovery/service.go index 97088310..dfb630e1 100644 --- a/api/pkg/discovery/service.go +++ b/api/pkg/discovery/service.go @@ -137,7 +137,6 @@ func (s *RegistryService) Start() { } s.logInfo("Discovery registry service starting", fields...) for _, ch := range s.consumers { - ch := ch go func() { if err := ch.consumer.ConsumeMessages(ch.handler); err != nil { s.logger.Warn("Discovery consumer stopped with error", zap.String("event", ch.event), zap.Error(err)) diff --git a/api/pkg/messaging/internal/natsb/broker_test.go b/api/pkg/messaging/internal/natsb/broker_test.go index 4152a962..da75fd10 100644 --- a/api/pkg/messaging/internal/natsb/broker_test.go +++ b/api/pkg/messaging/internal/natsb/broker_test.go @@ -11,6 +11,7 @@ func TestBuildSafePublishableNATSURL(t *testing.T) { t.Run("redacts single URL credentials", func(t *testing.T) { t.Parallel() + //nolint:gosec // Test fixture includes credentials to verify redaction logic. raw := "nats://alice:supersecret@localhost:4222" sanitized := buildSafePublishableNATSURL(raw) @@ -25,6 +26,7 @@ func TestBuildSafePublishableNATSURL(t *testing.T) { t.Run("redacts credentials in gateway URL format", func(t *testing.T) { t.Parallel() + //nolint:gosec // Test fixture includes credentials to verify redaction logic. raw := "nats://dev_nats:nats_password_123@dev-nats:4222" sanitized := buildSafePublishableNATSURL(raw) @@ -49,6 +51,7 @@ func TestBuildSafePublishableNATSURL(t *testing.T) { t.Run("redacts each URL in server list", func(t *testing.T) { t.Parallel() + //nolint:gosec // Test fixture includes credentials to verify redaction logic. raw := " nats://alice:one@localhost:4222, nats://bob:two@localhost:4223 " sanitized := buildSafePublishableNATSURL(raw) @@ -73,6 +76,7 @@ func TestBuildSafePublishableNATSURL(t *testing.T) { t.Run("redacts malformed URL credentials via fallback", func(t *testing.T) { t.Parallel() + //nolint:gosec // Test fixture includes credentials to verify redaction logic. raw := "nats://alice:pa%ss@localhost:4222" sanitized := buildSafePublishableNATSURL(raw) diff --git a/api/pkg/messaging/internal/notifications/account/notification.go b/api/pkg/messaging/internal/notifications/account/notification.go index db957a3d..1dfc8f81 100644 --- a/api/pkg/messaging/internal/notifications/account/notification.go +++ b/api/pkg/messaging/internal/notifications/account/notification.go @@ -24,7 +24,7 @@ func (acn *AccountNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return acn.Envelope.Wrap(data) + return acn.Wrap(data) } func NewAccountNotification(action nm.NotificationAction) model.NotificationEvent { diff --git a/api/pkg/messaging/internal/notifications/account/password_reset.go b/api/pkg/messaging/internal/notifications/account/password_reset.go index bd5fe763..2e33a5f6 100644 --- a/api/pkg/messaging/internal/notifications/account/password_reset.go +++ b/api/pkg/messaging/internal/notifications/account/password_reset.go @@ -24,7 +24,7 @@ func (prn *PasswordResetNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return prn.Envelope.Wrap(data) + return prn.Wrap(data) } func NewPasswordResetNotification(action nm.NotificationAction) model.NotificationEvent { diff --git a/api/pkg/messaging/internal/notifications/confirmation/notification.go b/api/pkg/messaging/internal/notifications/confirmation/notification.go index bd6b5abc..094d1ff7 100644 --- a/api/pkg/messaging/internal/notifications/confirmation/notification.go +++ b/api/pkg/messaging/internal/notifications/confirmation/notification.go @@ -27,7 +27,7 @@ func (ccn *ConfirmationCodeNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return ccn.Envelope.Wrap(data) + return ccn.Wrap(data) } func newConfirmationEvent(action nm.NotificationAction) model.NotificationEvent { diff --git a/api/pkg/messaging/internal/notifications/confirmations/notification.go b/api/pkg/messaging/internal/notifications/confirmations/notification.go index 87d34b3e..85e10661 100644 --- a/api/pkg/messaging/internal/notifications/confirmations/notification.go +++ b/api/pkg/messaging/internal/notifications/confirmations/notification.go @@ -20,7 +20,7 @@ func (crn *ConfirmationRequestNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return crn.Envelope.Wrap(data) + return crn.Wrap(data) } type ConfirmationResultNotification struct { @@ -33,7 +33,7 @@ func (crn *ConfirmationResultNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return crn.Envelope.Wrap(data) + return crn.Wrap(data) } type ConfirmationDispatchNotification struct { @@ -46,7 +46,7 @@ func (cdn *ConfirmationDispatchNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return cdn.Envelope.Wrap(data) + return cdn.Wrap(data) } func confirmationRequestEvent() model.NotificationEvent { diff --git a/api/pkg/messaging/internal/notifications/notification/notification.go b/api/pkg/messaging/internal/notifications/notification/notification.go index 3f92d058..a442086b 100644 --- a/api/pkg/messaging/internal/notifications/notification/notification.go +++ b/api/pkg/messaging/internal/notifications/notification/notification.go @@ -29,7 +29,7 @@ func (nrn *NResultNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return nrn.Envelope.Wrap(data) + return nrn.Wrap(data) } func NewNRNotification() model.NotificationEvent { diff --git a/api/pkg/messaging/internal/notifications/object/object.go b/api/pkg/messaging/internal/notifications/object/object.go index 689a68dc..42569f67 100644 --- a/api/pkg/messaging/internal/notifications/object/object.go +++ b/api/pkg/messaging/internal/notifications/object/object.go @@ -24,7 +24,7 @@ func (acn *ObjectNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return acn.Envelope.Wrap(data) + return acn.Wrap(data) } func NewObjectNotification(t mservice.Type, action nm.NotificationAction) model.NotificationEvent { diff --git a/api/pkg/messaging/internal/notifications/paymentgateway/notification.go b/api/pkg/messaging/internal/notifications/paymentgateway/notification.go index 2b7ea8e5..6caa3706 100644 --- a/api/pkg/messaging/internal/notifications/paymentgateway/notification.go +++ b/api/pkg/messaging/internal/notifications/paymentgateway/notification.go @@ -19,7 +19,7 @@ func (pgn *PaymentGatewayIntentNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return pgn.Envelope.Wrap(data) + return pgn.Wrap(data) } type PaymentGatewayExecutionNotification struct { @@ -32,7 +32,7 @@ func (pgn *PaymentGatewayExecutionNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return pgn.Envelope.Wrap(data) + return pgn.Wrap(data) } func intentEvent() model.NotificationEvent { diff --git a/api/pkg/messaging/internal/notifications/paymentorchestrator/notification.go b/api/pkg/messaging/internal/notifications/paymentorchestrator/notification.go index 4dee8798..2ed0d23a 100644 --- a/api/pkg/messaging/internal/notifications/paymentorchestrator/notification.go +++ b/api/pkg/messaging/internal/notifications/paymentorchestrator/notification.go @@ -20,7 +20,7 @@ func (psn *PaymentStatusUpdatedNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return psn.Envelope.Wrap(data) + return psn.Wrap(data) } func paymentStatusUpdatedEvent() model.NotificationEvent { diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index d496272b..85f562ab 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -74,7 +74,7 @@ func (srn *SiteRequestNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return srn.Envelope.Wrap(data) + return srn.Wrap(data) } func newSiteRequestEvent() model.NotificationEvent { diff --git a/api/pkg/messaging/internal/notifications/telegram/notification.go b/api/pkg/messaging/internal/notifications/telegram/notification.go index 1775b7bc..85d3a254 100644 --- a/api/pkg/messaging/internal/notifications/telegram/notification.go +++ b/api/pkg/messaging/internal/notifications/telegram/notification.go @@ -19,7 +19,7 @@ func (trn *TelegramReactionNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return trn.Envelope.Wrap(data) + return trn.Wrap(data) } func telegramReactionEvent() model.NotificationEvent { @@ -36,7 +36,7 @@ func (ttn *TelegramTextNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return ttn.Envelope.Wrap(data) + return ttn.Wrap(data) } func telegramTextEvent() model.NotificationEvent { @@ -53,7 +53,7 @@ func (tun *TelegramUpdateNotification) Serialize() ([]byte, error) { if err != nil { return nil, err } - return tun.Envelope.Wrap(data) + return tun.Wrap(data) } func telegramUpdateEvent() model.NotificationEvent { diff --git a/api/pkg/model/chainasset_test.go b/api/pkg/model/chainasset_test.go index 1de75361..ad54960f 100644 --- a/api/pkg/model/chainasset_test.go +++ b/api/pkg/model/chainasset_test.go @@ -13,9 +13,8 @@ func TestChainAssetDescriptionImplementsStorable(t *testing.T) { func TestChainAssetDescriptionCollection(t *testing.T) { var desc ChainAssetDescription - want := string(mservice.ChainAssets) + want := mservice.ChainAssets if got := desc.Collection(); got != want { t.Fatalf("Collection() = %q, want %q", got, want) } } - diff --git a/api/pkg/model/internal/notificationevent.go b/api/pkg/model/internal/notificationevent.go index 42c61e81..b83bf593 100644 --- a/api/pkg/model/internal/notificationevent.go +++ b/api/pkg/model/internal/notificationevent.go @@ -32,7 +32,7 @@ func (ne *NotificationEventImp) ToString() string { } func (ne *NotificationEventImp) StringType() string { - return string(ne.nType) + return ne.nType } func (ne *NotificationEventImp) StringAction() string { diff --git a/api/pkg/model/notificationevent.go b/api/pkg/model/notificationevent.go index 565afcba..91609436 100644 --- a/api/pkg/model/notificationevent.go +++ b/api/pkg/model/notificationevent.go @@ -44,7 +44,7 @@ func (ne *NotificationEventImp) ToString() string { } func (ne *NotificationEventImp) StringType() string { - return string(ne.nType) + return ne.nType } func (ne *NotificationEventImp) StringAction() string { diff --git a/api/pkg/mservice/services.go b/api/pkg/mservice/services.go index df0ec583..02f0c075 100644 --- a/api/pkg/mservice/services.go +++ b/api/pkg/mservice/services.go @@ -62,13 +62,13 @@ const ( ) func StringToSType(s string) (Type, error) { - switch Type(s) { + switch s { case Accounts, Verification, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, WalletRoutes, ChainWalletBalances, ChainTransfers, ChainDeposits, Callbacks, MntxGateway, PaymentGateway, FXOracle, FeePlans, BillingDocuments, FilterProjects, Invitations, Invoices, Logo, Ledger, LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications, Organizations, Payments, PaymentRoutes, PaymentOrchestrator, PaymentMethods, Permissions, Policies, PolicyAssignements, Recipients, RefreshTokens, Roles, Storage, Tenants, Workflows, Discovery, ChSettle: - return Type(s), nil + return s, nil default: return "", merrors.InvalidArgument("invalid service type", s) } diff --git a/api/pkg/mutil/fr/fr.go b/api/pkg/mutil/fr/fr.go index fd938fcf..045fd620 100644 --- a/api/pkg/mutil/fr/fr.go +++ b/api/pkg/mutil/fr/fr.go @@ -15,6 +15,7 @@ func CloseFile(logger mlogger.Logger, file *os.File) { } func ReadFile(logger mlogger.Logger, filePath string) ([]byte, error) { + //nolint:gosec // Read path is provided by trusted caller configuration. file, err := os.Open(filePath) if err != nil { logger.Warn("Failed to open file", zap.String("path", filePath), zap.Error(err)) diff --git a/api/pkg/mutil/http/http.go b/api/pkg/mutil/http/http.go index 1c6adb8e..6ad37c43 100644 --- a/api/pkg/mutil/http/http.go +++ b/api/pkg/mutil/http/http.go @@ -49,7 +49,11 @@ func SendAPIRequest(ctx context.Context, logger mlogger.Logger, httpMethod api.H logger.Warn("Failed to execute request", zap.Error(err), zap.String("method", method), zap.String("url", url), zap.Any("payload", payload)) return err } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + logger.Warn("Failed to close response body", zap.Error(closeErr), zap.String("method", method), zap.String("url", url)) + } + }() // Read the sresponse body body, err := io.ReadAll(resp.Body) diff --git a/api/pkg/mutil/reorder/reorder.go b/api/pkg/mutil/reorder/reorder.go index 14f02d6b..f53d8c9d 100644 --- a/api/pkg/mutil/reorder/reorder.go +++ b/api/pkg/mutil/reorder/reorder.go @@ -9,7 +9,7 @@ import ( // Returns the reordered slice with updated indices, or an error if indices are invalid func IndexableRefs(items []model.IndexableRef, oldIndex, newIndex int) ([]model.IndexableRef, error) { // Find the item to reorder - var targetIndex int = -1 + targetIndex := -1 for i, item := range items { if item.Index == oldIndex { targetIndex = i diff --git a/api/pkg/server/grpcapp/app.go b/api/pkg/server/grpcapp/app.go index bdd12d73..19f86ed4 100644 --- a/api/pkg/server/grpcapp/app.go +++ b/api/pkg/server/grpcapp/app.go @@ -156,8 +156,9 @@ func (a *App[T]) Start() error { } a.metricsSrv = &http.Server{ - Addr: addr, - Handler: router, + Addr: addr, + Handler: router, + ReadHeaderTimeout: 5 * time.Second, } go func() { a.logger.Info("Prometheus metrics server starting", zap.String("address", addr)) @@ -185,7 +186,7 @@ func (a *App[T]) Start() error { } a.logger.Debug("GRPC services registered") - a.runCtx, a.cancel = context.WithCancel(context.Background()) + a.runCtx, a.cancel = context.WithCancel(context.Background()) //nolint:gosec // Cancellation func is retained on app state and invoked on Shutdown. a.logger.Debug("GRPC server context initialised") if err := a.grpc.Start(a.runCtx); err != nil { @@ -219,9 +220,6 @@ func (a *App[T]) Start() error { } func (a *App[T]) Shutdown(ctx context.Context) { - if ctx == nil { - ctx = context.Background() - } if a.cancel != nil { a.cancel() } diff --git a/api/pkg/server/internal/server.go b/api/pkg/server/internal/server.go index a31035bc..049f00c4 100644 --- a/api/pkg/server/internal/server.go +++ b/api/pkg/server/internal/server.go @@ -30,11 +30,15 @@ func prepareLogger() mlogger.Logger { func RunServer(rootLoggerName string, av version.Printer, factory server.ServerFactoryT) { logger := prepareLogger().Named(rootLoggerName) logger = logger.With(zap.String("instance_id", discovery.InstanceID())) - defer logger.Sync() + defer func() { + _ = logger.Sync() + }() // Show version information if *versionFlag { - fmt.Fprintln(os.Stdout, av.Print()) + if _, err := fmt.Fprintln(os.Stdout, av.Print()); err != nil { + logger.Warn("Failed to print version", zap.Error(err)) + } return } diff --git a/api/pkg/vault/kv/service.go b/api/pkg/vault/kv/service.go index 7b6fd651..2b29867d 100644 --- a/api/pkg/vault/kv/service.go +++ b/api/pkg/vault/kv/service.go @@ -161,6 +161,7 @@ func resolveToken(config Config) (string, string, error) { } } if tokenFilePath != "" { + //nolint:gosec // Token file path comes from trusted deployment configuration. raw, err := os.ReadFile(tokenFilePath) if err != nil { return "", "", merrors.Internal("vault kv: failed to read token file " + tokenFilePath + ": " + err.Error())