This commit is contained in:
Stephan D
2026-03-10 12:31:09 +01:00
parent d87e709f43
commit e77d1ab793
287 changed files with 2089 additions and 1550 deletions

2
.gitignore vendored
View File

@@ -10,7 +10,9 @@ generate_protos.sh
update_dep.sh
test.sh
.vscode/
.gocache/
.golangci-cache/
.cache/
.claude/

View File

@@ -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)"

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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),
)
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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,
}
}

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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))
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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()),

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -90,5 +90,5 @@ func (a *PermissionsAPI) changePoliciesImp(
}
}
return nil, nil
return struct{}{}, nil
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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() {

View File

@@ -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()))}
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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))

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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.

View File

@@ -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 := &currencyStore{logger: zap.NewNop(), repo: repo}

View File

@@ -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}

View File

@@ -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)
},
}

View File

@@ -176,16 +176,18 @@ func cloneCurrency(t *testing.T, obj storable.Storable) *model.Currency {
return &copy
}
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()
}
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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 = ""
}

View File

@@ -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)),

View File

@@ -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")

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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])
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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),

View File

@@ -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

View File

@@ -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{}{

View File

@@ -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",
})

View File

@@ -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)

View File

@@ -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{

View File

@@ -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)

View File

@@ -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())

View File

@@ -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{}) {

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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
}

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