linting
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,7 +10,9 @@ generate_protos.sh
|
||||
update_dep.sh
|
||||
test.sh
|
||||
.vscode/
|
||||
|
||||
.gocache/
|
||||
.golangci-cache/
|
||||
.cache/
|
||||
.claude/
|
||||
|
||||
|
||||
20
Makefile
20
Makefile
@@ -1,10 +1,12 @@
|
||||
# Sendico Development Environment - Makefile
|
||||
# Docker Compose + Makefile build system
|
||||
|
||||
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend backend-up backend-down backend-rebuild
|
||||
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend lint-api backend-up backend-down backend-rebuild
|
||||
|
||||
COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev
|
||||
SERVICE ?=
|
||||
API_GOCACHE ?= $(CURDIR)/.gocache
|
||||
API_GOLANGCI_LINT_CACHE ?= $(CURDIR)/.golangci-cache
|
||||
BACKEND_SERVICES := \
|
||||
dev-discovery \
|
||||
dev-fx-oracle \
|
||||
@@ -76,6 +78,7 @@ help:
|
||||
@echo " make test Run all tests (API + frontend)"
|
||||
@echo " make test-api Run Go API tests only"
|
||||
@echo " make test-frontend Run Flutter tests only"
|
||||
@echo " make lint-api Run golangci-lint across all API Go modules"
|
||||
@echo " make health Check service health"
|
||||
@echo ""
|
||||
@echo "Examples:"
|
||||
@@ -374,3 +377,18 @@ test-frontend:
|
||||
@cd frontend/pshared && flutter test
|
||||
@cd frontend/pweb && flutter test
|
||||
@echo "$(GREEN)✅ All frontend tests passed$(NC)"
|
||||
|
||||
# Run Go API linting
|
||||
lint-api:
|
||||
@echo "$(GREEN)Running API linting...$(NC)"
|
||||
@mkdir -p "$(API_GOCACHE)" "$(API_GOLANGCI_LINT_CACHE)"
|
||||
@failed=""; \
|
||||
for dir in $$(find api -name go.mod -exec dirname {} \;); do \
|
||||
echo "Linting $$dir..."; \
|
||||
(cd "$$dir" && GOCACHE="$(API_GOCACHE)" GOLANGCI_LINT_CACHE="$(API_GOLANGCI_LINT_CACHE)" golangci-lint run --allow-serial-runners --allow-parallel-runners ./...) || failed="$$failed $$dir"; \
|
||||
done; \
|
||||
if [ -n "$$failed" ]; then \
|
||||
echo "$(YELLOW)Lint failed:$$failed$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(GREEN)✅ All API lint checks passed$(NC)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
47
api/edge/bff/.golangci.yml
Normal file
47
api/edge/bff/.golangci.yml
Normal 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
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -90,5 +90,5 @@ func (a *PermissionsAPI) changePoliciesImp(
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return struct{}{}, nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
47
api/edge/callbacks/.golangci.yml
Normal file
47
api/edge/callbacks/.golangci.yml
Normal 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
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()))}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
47
api/fx/oracle/.golangci.yml
Normal file
47
api/fx/oracle/.golangci.yml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
47
api/fx/storage/.golangci.yml
Normal file
47
api/fx/storage/.golangci.yml
Normal 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
|
||||
@@ -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.
|
||||
|
||||
@@ -35,8 +35,8 @@ func TestCurrencyStoreGet(t *testing.T) {
|
||||
|
||||
func TestCurrencyStoreList(t *testing.T) {
|
||||
repo := &repoStub{
|
||||
findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error {
|
||||
return runDecoderWithDocs(t, decode, &model.Currency{Code: "USD"})
|
||||
findManyFn: func(ctx context.Context, _ builder.Query, decode rd.DecodingFunc) error {
|
||||
return runDecoderWithDocs(ctx, t, decode, &model.Currency{Code: "USD"})
|
||||
},
|
||||
}
|
||||
store := ¤cyStore{logger: zap.NewNop(), repo: repo}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -176,16 +176,18 @@ func cloneCurrency(t *testing.T, obj storable.Storable) *model.Currency {
|
||||
return ©
|
||||
}
|
||||
|
||||
func runDecoderWithDocs(t *testing.T, decode rd.DecodingFunc, docs ...interface{}) error {
|
||||
func runDecoderWithDocs(ctx context.Context, t *testing.T, decode rd.DecodingFunc, docs ...interface{}) error {
|
||||
t.Helper()
|
||||
cur, err := mongo.NewCursorFromDocuments(docs, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create cursor: %v", err)
|
||||
}
|
||||
defer cur.Close(context.Background())
|
||||
defer func() {
|
||||
_ = cur.Close(ctx)
|
||||
}()
|
||||
|
||||
if len(docs) > 0 {
|
||||
if !cur.Next(context.Background()) {
|
||||
if !cur.Next(ctx) {
|
||||
return cur.Err()
|
||||
}
|
||||
}
|
||||
|
||||
47
api/gateway/aurora/.golangci.yml
Normal file
47
api/gateway/aurora/.golangci.yml
Normal 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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = ""
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
47
api/gateway/chain/.golangci.yml
Normal file
47
api/gateway/chain/.golangci.yml
Normal 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
|
||||
@@ -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{}{
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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{}) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user