diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod
index a72b51c..18ca5d2 100644
--- a/api/billing/fees/go.mod
+++ b/api/billing/fees/go.mod
@@ -46,9 +46,9 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/protobuf v1.36.10
)
diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum
index d63f35a..4d80caf 100644
--- a/api/billing/fees/go.sum
+++ b/api/billing/fees/go.sum
@@ -187,24 +187,24 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/api/fx/ingestor/config.yml b/api/fx/ingestor/config.yml
index df025dc..9b352e5 100644
--- a/api/fx/ingestor/config.yml
+++ b/api/fx/ingestor/config.yml
@@ -8,6 +8,9 @@ market:
- driver: COINGECKO
settings:
base_url: "https://api.coingecko.com/api/v3"
+ - driver: CBR
+ settings:
+ base_url: "https://www.cbr.ru"
pairs:
BINANCE:
- base: "USDT"
@@ -26,6 +29,15 @@ market:
- base: "USDT"
quote: "RUB"
symbol: "tether:rub"
+ CBR:
+ - base: "USD"
+ quote: "RUB"
+ symbol: "USD"
+ provider: "cbr"
+ - base: "EUR"
+ quote: "RUB"
+ symbol: "EUR"
+ provider: "cbr"
metrics:
enabled: true
diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod
index 0de7c77..4307706 100644
--- a/api/fx/ingestor/go.mod
+++ b/api/fx/ingestor/go.mod
@@ -13,6 +13,7 @@ require (
github.com/tech/sendico/fx/storage v0.0.0
github.com/tech/sendico/pkg v0.1.0
go.uber.org/zap v1.27.1
+ golang.org/x/net v0.47.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -45,10 +46,9 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
- golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum
index d63f35a..4d80caf 100644
--- a/api/fx/ingestor/go.sum
+++ b/api/fx/ingestor/go.sum
@@ -187,24 +187,24 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/api/fx/ingestor/internal/app/app.go b/api/fx/ingestor/internal/app/app.go
index 28ab12d..38f2055 100644
--- a/api/fx/ingestor/internal/app/app.go
+++ b/api/fx/ingestor/internal/app/app.go
@@ -7,12 +7,12 @@ import (
"github.com/tech/sendico/fx/ingestor/internal/appversion"
"github.com/tech/sendico/fx/ingestor/internal/config"
- "github.com/tech/sendico/fx/ingestor/internal/fmerrors"
"github.com/tech/sendico/fx/ingestor/internal/ingestor"
"github.com/tech/sendico/fx/ingestor/internal/metrics"
mongostorage "github.com/tech/sendico/fx/storage/mongo"
"github.com/tech/sendico/pkg/api/routers/health"
"github.com/tech/sendico/pkg/db"
+ "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
@@ -26,7 +26,7 @@ type App struct {
func New(logger mlogger.Logger, cfgPath string) (*App, error) {
if logger == nil {
- return nil, fmerrors.New("app: logger is nil")
+ return nil, merrors.InvalidArgument("app: logger is nil")
}
path := strings.TrimSpace(cfgPath)
if path == "" {
diff --git a/api/fx/ingestor/internal/config/config.go b/api/fx/ingestor/internal/config/config.go
index d01f31c..53394bf 100644
--- a/api/fx/ingestor/internal/config/config.go
+++ b/api/fx/ingestor/internal/config/config.go
@@ -5,9 +5,9 @@ import (
"strings"
"time"
- "github.com/tech/sendico/fx/ingestor/internal/fmerrors"
mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
"github.com/tech/sendico/pkg/db"
+ "github.com/tech/sendico/pkg/merrors"
"gopkg.in/yaml.v3"
)
@@ -25,33 +25,33 @@ type Config struct {
func Load(path string) (*Config, error) {
if path == "" {
- return nil, fmerrors.New("config: path is empty")
+ return nil, merrors.InvalidArgument("config: path is empty")
}
data, err := os.ReadFile(path)
if err != nil {
- return nil, fmerrors.Wrap("config: failed to read file", err)
+ return nil, merrors.InternalWrap(err, "config: failed to read file")
}
cfg := &Config{}
if err := yaml.Unmarshal(data, cfg); err != nil {
- return nil, fmerrors.Wrap("config: failed to parse yaml", err)
+ return nil, merrors.InternalWrap(err, "config: failed to parse yaml")
}
if len(cfg.Market.Sources) == 0 {
- return nil, fmerrors.New("config: no market sources configured")
+ return nil, merrors.InvalidArgument("config: no market sources configured")
}
sourceSet := make(map[mmodel.Driver]struct{}, len(cfg.Market.Sources))
for idx := range cfg.Market.Sources {
src := &cfg.Market.Sources[idx]
if src.Driver.IsEmpty() {
- return nil, fmerrors.New("config: market source driver is empty")
+ return nil, merrors.InvalidArgument("config: market source driver is empty")
}
sourceSet[src.Driver] = struct{}{}
}
if len(cfg.Market.Pairs) == 0 {
- return nil, fmerrors.New("config: no pairs configured")
+ return nil, merrors.InvalidArgument("config: no pairs configured")
}
normalizedPairs := make(map[string][]PairConfig, len(cfg.Market.Pairs))
@@ -61,10 +61,10 @@ func Load(path string) (*Config, error) {
for rawSource, pairList := range cfg.Market.Pairs {
driver := mmodel.Driver(rawSource)
if driver.IsEmpty() {
- return nil, fmerrors.New("config: pair source is empty")
+ return nil, merrors.InvalidArgument("config: pair source is empty")
}
if _, ok := sourceSet[driver]; !ok {
- return nil, fmerrors.New("config: pair references unknown source: " + driver.String())
+ return nil, merrors.InvalidArgument("config: pair references unknown source: "+driver.String(), "pairs."+driver.String())
}
processed := make([]PairConfig, len(pairList))
@@ -74,7 +74,7 @@ func Load(path string) (*Config, error) {
pair.Quote = strings.ToUpper(strings.TrimSpace(pair.Quote))
pair.Symbol = strings.TrimSpace(pair.Symbol)
if pair.Base == "" || pair.Quote == "" || pair.Symbol == "" {
- return nil, fmerrors.New("config: pair entries must define base, quote, and symbol")
+ 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())
@@ -93,7 +93,7 @@ func Load(path string) (*Config, error) {
cfg.pairsBySource = pairsBySource
cfg.pairs = flattened
if cfg.Database == nil {
- return nil, fmerrors.New("config: database configuration is required")
+ return nil, merrors.InvalidArgument("config: database configuration is required")
}
if cfg.Metrics != nil && cfg.Metrics.Enabled {
diff --git a/api/fx/ingestor/internal/fmerrors/market.go b/api/fx/ingestor/internal/fmerrors/market.go
deleted file mode 100644
index a21ba63..0000000
--- a/api/fx/ingestor/internal/fmerrors/market.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package fmerrors
-
-type Error struct {
- message string
- cause error
-}
-
-func (e *Error) Error() string {
- if e == nil {
- return ""
- }
- if e.cause == nil {
- return e.message
- }
- return e.message + ": " + e.cause.Error()
-}
-
-func (e *Error) Unwrap() error {
- if e == nil {
- return nil
- }
- return e.cause
-}
-
-func New(message string) error {
- return &Error{message: message}
-}
-
-func Wrap(message string, cause error) error {
- return &Error{message: message, cause: cause}
-}
-
-func NewDecimal(value string) error {
- return &Error{message: "invalid decimal \"" + value + "\""}
-}
diff --git a/api/fx/ingestor/internal/ingestor/service.go b/api/fx/ingestor/internal/ingestor/service.go
index a3f8c54..75bc752 100644
--- a/api/fx/ingestor/internal/ingestor/service.go
+++ b/api/fx/ingestor/internal/ingestor/service.go
@@ -6,11 +6,11 @@ import (
"time"
"github.com/tech/sendico/fx/ingestor/internal/config"
- "github.com/tech/sendico/fx/ingestor/internal/fmerrors"
"github.com/tech/sendico/fx/ingestor/internal/market"
mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
"github.com/tech/sendico/fx/storage"
"github.com/tech/sendico/fx/storage/model"
+ "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
@@ -26,18 +26,18 @@ type Service struct {
func New(logger mlogger.Logger, cfg *config.Config, repo storage.Repository) (*Service, error) {
if logger == nil {
- return nil, fmerrors.New("ingestor: nil logger")
+ return nil, merrors.InvalidArgument("ingestor: nil logger")
}
if cfg == nil {
- return nil, fmerrors.New("ingestor: nil config")
+ return nil, merrors.InvalidArgument("ingestor: nil config")
}
if repo == nil {
- return nil, fmerrors.New("ingestor: nil repository")
+ return nil, merrors.InvalidArgument("ingestor: nil repository")
}
connectors, err := market.BuildConnectors(logger, cfg.Market.Sources)
if err != nil {
- return nil, fmerrors.Wrap("build connectors", err)
+ return nil, merrors.InternalWrap(err, "build connectors")
}
return &Service{
@@ -110,21 +110,21 @@ func (s *Service) pollOnce(ctx context.Context) error {
func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error {
connector, ok := s.connectors[pair.Source]
if !ok {
- return fmerrors.Wrap("connector not configured for source "+pair.Source.String(), nil)
+ return merrors.InvalidArgument("connector not configured for source "+pair.Source.String(), "source")
}
ticker, err := connector.FetchTicker(ctx, pair.Symbol)
if err != nil {
- return fmerrors.Wrap("fetch ticker", err)
+ return merrors.InternalWrap(err, "fetch ticker")
}
bid, err := parseDecimal(ticker.BidPrice)
if err != nil {
- return fmerrors.Wrap("parse bid price", err)
+ return merrors.InvalidArgumentWrap(err, "parse bid price", "bid")
}
ask, err := parseDecimal(ticker.AskPrice)
if err != nil {
- return fmerrors.Wrap("parse ask price", err)
+ return merrors.InvalidArgumentWrap(err, "parse ask price", "ask")
}
if pair.Invert {
@@ -166,7 +166,7 @@ func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error {
}
if err := s.rates.UpsertSnapshot(ctx, snapshot); err != nil {
- return fmerrors.Wrap("upsert snapshot", err)
+ return merrors.InternalWrap(err, "upsert snapshot")
}
s.logger.Debug("Snapshot ingested",
@@ -183,7 +183,7 @@ func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error {
func parseDecimal(value string) (*big.Rat, error) {
r := new(big.Rat)
if _, ok := r.SetString(value); !ok {
- return nil, fmerrors.NewDecimal(value)
+ return nil, merrors.InvalidArgument("invalid decimal \""+value+"\"", "value")
}
return r, nil
}
diff --git a/api/fx/ingestor/internal/ingestor/service_test.go b/api/fx/ingestor/internal/ingestor/service_test.go
index 63fc931..16f9b2e 100644
--- a/api/fx/ingestor/internal/ingestor/service_test.go
+++ b/api/fx/ingestor/internal/ingestor/service_test.go
@@ -7,10 +7,10 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/tech/sendico/fx/ingestor/internal/config"
- "github.com/tech/sendico/fx/ingestor/internal/fmerrors"
mmarket "github.com/tech/sendico/fx/ingestor/internal/model"
"github.com/tech/sendico/fx/storage"
"github.com/tech/sendico/fx/storage/model"
+ "github.com/tech/sendico/pkg/merrors"
"go.uber.org/zap"
)
@@ -131,7 +131,7 @@ func TestServiceUpsertPairInvertsPrices(t *testing.T) {
}
func TestServicePollOnceReturnsFirstError(t *testing.T) {
- errFetch := fmerrors.New("fetch failed")
+ errFetch := merrors.Internal("fetch failed")
connectorSuccess := &connectorStub{
id: mmarket.DriverBinance,
ticker: &mmarket.Ticker{
diff --git a/api/fx/ingestor/internal/market/binance/connector.go b/api/fx/ingestor/internal/market/binance/connector.go
index f46e131..809d89d 100644
--- a/api/fx/ingestor/internal/market/binance/connector.go
+++ b/api/fx/ingestor/internal/market/binance/connector.go
@@ -10,9 +10,9 @@ import (
"strings"
"time"
- "github.com/tech/sendico/fx/ingestor/internal/fmerrors"
"github.com/tech/sendico/fx/ingestor/internal/market/common"
mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
+ "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
@@ -60,7 +60,7 @@ func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Conne
parsed, err := url.Parse(baseURL)
if err != nil {
- return nil, fmerrors.Wrap("binance: invalid base url", err)
+ return nil, merrors.InvalidArgumentWrap(err, "binance: invalid base url", "base_url")
}
transport := &http.Transport{
@@ -89,12 +89,12 @@ func (c *binanceConnector) ID() mmodel.Driver {
func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmodel.Ticker, error) {
if strings.TrimSpace(symbol) == "" {
- return nil, fmerrors.New("binance: symbol is empty")
+ return nil, merrors.InvalidArgument("binance: symbol is empty", "symbol")
}
endpoint, err := url.Parse(c.base)
if err != nil {
- return nil, fmerrors.Wrap("binance: parse base url", err)
+ return nil, merrors.InternalWrap(err, "binance: parse base url")
}
endpoint.Path = "/api/v3/ticker/bookTicker"
query := endpoint.Query()
@@ -103,19 +103,19 @@ func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmo
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
- return nil, fmerrors.Wrap("binance: build request", err)
+ return nil, merrors.InternalWrap(err, "binance: build request")
}
resp, err := c.client.Do(req)
if err != nil {
c.logger.Warn("Binance request failed", zap.String("symbol", symbol), zap.Error(err))
- return nil, fmerrors.Wrap("binance: request failed", err)
+ return nil, merrors.InternalWrap(err, "binance: request failed")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.logger.Warn("Binance returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode))
- return nil, fmerrors.New("binance: unexpected status " + strconv.Itoa(resp.StatusCode))
+ return nil, merrors.Internal("binance: unexpected status " + strconv.Itoa(resp.StatusCode))
}
var payload struct {
@@ -126,7 +126,7 @@ func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmo
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
c.logger.Warn("Binance decode failed", zap.String("symbol", symbol), zap.Error(err))
- return nil, fmerrors.Wrap("binance: decode response", err)
+ return nil, merrors.InternalWrap(err, "binance: decode response")
}
return &mmodel.Ticker{
diff --git a/api/fx/ingestor/internal/market/cbr/connector.go b/api/fx/ingestor/internal/market/cbr/connector.go
new file mode 100644
index 0000000..ed4f6fb
--- /dev/null
+++ b/api/fx/ingestor/internal/market/cbr/connector.go
@@ -0,0 +1,537 @@
+package cbr
+
+import (
+ "context"
+ "encoding/xml"
+ "math/big"
+ "net"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/tech/sendico/fx/ingestor/internal/market/common"
+ mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
+ "github.com/tech/sendico/pkg/merrors"
+ "github.com/tech/sendico/pkg/mlogger"
+ "github.com/tech/sendico/pkg/model"
+ "go.uber.org/zap"
+ "golang.org/x/net/html/charset"
+)
+
+type cbrConnector struct {
+ id mmodel.Driver
+ provider string
+ client *http.Client
+ base string
+ dailyPath string
+ directoryPath string
+ dynamicPath string
+ logger mlogger.Logger
+
+ byISO map[string]valuteInfo
+ byID map[string]valuteInfo
+}
+
+const defaultCBRBaseURL = "https://www.cbr.ru"
+const (
+ defaultDirectoryPath = "/scripts/XML_valFull.asp"
+ defaultDailyPath = "/scripts/XML_daily.asp"
+ defaultDynamicPath = "/scripts/XML_dynamic.asp"
+)
+
+const (
+ defaultDialTimeoutSeconds = 5 * time.Second
+ defaultDialKeepAliveSeconds = 30 * time.Second
+ defaultTLSHandshakeTimeoutSeconds = 5 * time.Second
+ defaultResponseHeaderTimeoutSeconds = 10 * time.Second
+ defaultRequestTimeoutSeconds = 10 * time.Second
+)
+
+func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) {
+ baseURL := defaultCBRBaseURL
+ provider := strings.ToLower(mmodel.DriverCBR.String())
+ dialTimeout := defaultDialTimeoutSeconds
+ dialKeepAlive := defaultDialKeepAliveSeconds
+ tlsHandshakeTimeout := defaultTLSHandshakeTimeoutSeconds
+ responseHeaderTimeout := defaultResponseHeaderTimeoutSeconds
+ requestTimeout := defaultRequestTimeoutSeconds
+ directoryPath := defaultDirectoryPath
+ dailyPath := defaultDailyPath
+ dynamicPath := defaultDynamicPath
+
+ if settings != nil {
+ if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" {
+ baseURL = strings.TrimSpace(value)
+ }
+ if value, ok := settings["provider"].(string); ok && strings.TrimSpace(value) != "" {
+ provider = strings.TrimSpace(value)
+ }
+ if value, ok := settings["directory_path"].(string); ok && strings.TrimSpace(value) != "" {
+ directoryPath = strings.TrimSpace(value)
+ }
+ if value, ok := settings["daily_path"].(string); ok && strings.TrimSpace(value) != "" {
+ dailyPath = strings.TrimSpace(value)
+ }
+ if value, ok := settings["dynamic_path"].(string); ok && strings.TrimSpace(value) != "" {
+ dynamicPath = strings.TrimSpace(value)
+ }
+ dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout)
+ dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive)
+ tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout)
+ responseHeaderTimeout = common.DurationSetting(settings, "response_header_timeout_seconds", responseHeaderTimeout)
+ requestTimeout = common.DurationSetting(settings, "request_timeout_seconds", requestTimeout)
+ }
+
+ parsed, err := url.Parse(baseURL)
+ if err != nil {
+ return nil, merrors.InvalidArgumentWrap(err, "cbr: invalid base url", "base_url")
+ }
+
+ var transport http.RoundTripper = &http.Transport{
+ DialContext: (&net.Dialer{Timeout: dialTimeout, KeepAlive: dialKeepAlive}).DialContext,
+ TLSHandshakeTimeout: tlsHandshakeTimeout,
+ ResponseHeaderTimeout: responseHeaderTimeout,
+ }
+
+ if customTransport, ok := settings["http_round_tripper"].(http.RoundTripper); ok && customTransport != nil {
+ transport = customTransport
+ }
+
+ connector := &cbrConnector{
+ id: mmodel.DriverCBR,
+ provider: provider,
+ client: &http.Client{
+ Timeout: requestTimeout,
+ Transport: transport,
+ },
+ base: strings.TrimRight(parsed.String(), "/"),
+ dailyPath: dailyPath,
+ directoryPath: directoryPath,
+ dynamicPath: dynamicPath,
+ logger: logger.Named("cbr"),
+ }
+
+ if err := connector.refreshDirectory(); err != nil {
+ return nil, err
+ }
+
+ return connector, nil
+}
+
+func (c *cbrConnector) ID() mmodel.Driver {
+ return c.id
+}
+
+func (c *cbrConnector) FetchTicker(ctx context.Context, symbol string) (*mmodel.Ticker, error) {
+ isoCode, asOfDate, err := parseSymbol(symbol)
+ if err != nil {
+ return nil, err
+ }
+
+ valute, ok := c.byISO[isoCode]
+ if !ok {
+ return nil, merrors.InvalidArgument("cbr: unknown currency "+isoCode, "symbol")
+ }
+
+ var price string
+ if asOfDate != nil {
+ price, err = c.fetchHistoricalRate(ctx, valute, *asOfDate)
+ } else {
+ price, err = c.fetchDailyRate(ctx, valute)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ now := time.Now().UnixMilli()
+ return &mmodel.Ticker{
+ Symbol: formatSymbol(isoCode, asOfDate),
+ BidPrice: price,
+ AskPrice: price,
+ Provider: c.provider,
+ Timestamp: now,
+ }, nil
+}
+
+func (c *cbrConnector) refreshDirectory() error {
+ endpoint, err := c.buildURL(c.directoryPath, nil)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest(http.MethodGet, endpoint, nil)
+ if err != nil {
+ return merrors.InternalWrap(err, "cbr: build directory request")
+ }
+
+ resp, err := c.client.Do(req)
+ if err != nil {
+ c.logger.Warn("CBR directory request failed", zap.Error(err))
+ return merrors.InternalWrap(err, "cbr: directory request failed")
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ c.logger.Warn("CBR directory returned non-OK status", zap.Int("status", resp.StatusCode))
+ return merrors.Internal("cbr: unexpected status " + strconv.Itoa(resp.StatusCode))
+ }
+
+ decoder := xml.NewDecoder(resp.Body)
+ decoder.CharsetReader = charset.NewReaderLabel
+
+ var directory valuteDirectory
+ if err := decoder.Decode(&directory); err != nil {
+ c.logger.Warn("CBR directory decode failed", zap.Error(err))
+ return merrors.InternalWrap(err, "cbr: decode directory")
+ }
+
+ mapping, err := buildValuteMapping(directory.Items)
+ if err != nil {
+ return err
+ }
+
+ c.byISO = mapping.byISO
+ c.byID = mapping.byID
+ return nil
+}
+
+func (c *cbrConnector) fetchDailyRate(ctx context.Context, valute valuteInfo) (string, error) {
+ endpoint, err := c.buildURL(c.dailyPath, nil)
+ if err != nil {
+ return "", err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+ if err != nil {
+ return "", merrors.InternalWrap(err, "cbr: build daily request")
+ }
+
+ resp, err := c.client.Do(req)
+ if err != nil {
+ c.logger.Warn("CBR daily request failed", zap.String("currency", valute.ISOCharCode), zap.Error(err))
+ return "", merrors.InternalWrap(err, "cbr: daily request failed")
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ c.logger.Warn("CBR daily returned non-OK status", zap.Int("status", resp.StatusCode))
+ return "", merrors.Internal("cbr: unexpected status " + strconv.Itoa(resp.StatusCode))
+ }
+
+ decoder := xml.NewDecoder(resp.Body)
+ decoder.CharsetReader = charset.NewReaderLabel
+
+ var payload dailyRates
+ if err := decoder.Decode(&payload); err != nil {
+ c.logger.Warn("CBR daily decode failed", zap.Error(err))
+ return "", merrors.InternalWrap(err, "cbr: decode daily response")
+ }
+
+ entry := payload.find(valute.ID)
+ if entry == nil {
+ return "", merrors.NoData("cbr: currency not found in daily rates: " + valute.ISOCharCode)
+ }
+
+ if err := validateDailyEntry(valute, entry); err != nil {
+ return "", err
+ }
+
+ return computePrice(entry.Value, entry.Nominal)
+}
+
+func (c *cbrConnector) fetchHistoricalRate(ctx context.Context, valute valuteInfo, date time.Time) (string, error) {
+ query := map[string]string{
+ "date_req1": date.Format("02/01/2006"),
+ "date_req2": date.Format("02/01/2006"),
+ "VAL_NM_RQ": valute.ID,
+ }
+ endpoint, err := c.buildURL(c.dynamicPath, query)
+ if err != nil {
+ return "", err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+ if err != nil {
+ return "", merrors.InternalWrap(err, "cbr: build historical request")
+ }
+
+ resp, err := c.client.Do(req)
+ if err != nil {
+ c.logger.Warn("CBR historical request failed", zap.String("currency", valute.ISOCharCode), zap.Error(err))
+ return "", merrors.InternalWrap(err, "cbr: historical request failed")
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ c.logger.Warn("CBR historical returned non-OK status", zap.Int("status", resp.StatusCode))
+ return "", merrors.Internal("cbr: unexpected status " + strconv.Itoa(resp.StatusCode))
+ }
+
+ decoder := xml.NewDecoder(resp.Body)
+ decoder.CharsetReader = charset.NewReaderLabel
+
+ var payload dynamicRates
+ if err := decoder.Decode(&payload); err != nil {
+ c.logger.Warn("CBR historical decode failed", zap.Error(err))
+ return "", merrors.InternalWrap(err, "cbr: decode historical response")
+ }
+
+ record := payload.find(valute.ID, date)
+ if record == nil {
+ return "", merrors.NoData("cbr: historical rate not found for " + valute.ISOCharCode)
+ }
+
+ if record.Nominal != "" {
+ nominal, err := parseNominal(record.Nominal)
+ if err != nil {
+ return "", merrors.InvalidDataType(err.Error())
+ }
+ if nominal != valute.Nominal {
+ return "", merrors.Internal("cbr: historical nominal mismatch for " + valute.ISOCharCode)
+ }
+ }
+
+ return computePrice(record.Value, strconv.FormatInt(valute.Nominal, 10))
+}
+
+func (c *cbrConnector) buildURL(path string, query map[string]string) (string, error) {
+ base, err := url.Parse(c.base)
+ if err != nil {
+ return "", merrors.InternalWrap(err, "cbr: parse base url")
+ }
+ base.Path = strings.TrimRight(base.Path, "/") + path
+ q := base.Query()
+ for key, value := range query {
+ q.Set(key, value)
+ }
+ base.RawQuery = q.Encode()
+ return base.String(), nil
+}
+
+type valuteDirectory struct {
+ Items []valuteItem `xml:"Item"`
+}
+
+type valuteItem struct {
+ ID string `xml:"ID,attr"`
+ ISOChar string `xml:"ISO_Char_Code"`
+ ISONum string `xml:"ISO_Num_Code"`
+ Name string `xml:"Name"`
+ EngName string `xml:"EngName"`
+ NominalStr string `xml:"Nominal"`
+}
+
+type valuteInfo struct {
+ ID string
+ ISOCharCode string
+ ISONumCode string
+ Name string
+ EngName string
+ Nominal int64
+}
+
+type valuteMapping struct {
+ byISO map[string]valuteInfo
+ byID map[string]valuteInfo
+}
+
+func buildValuteMapping(items []valuteItem) (*valuteMapping, error) {
+ byISO := make(map[string]valuteInfo, len(items))
+ byID := make(map[string]valuteInfo, len(items))
+ byNum := make(map[string]string, len(items))
+
+ for _, item := range items {
+ id := strings.TrimSpace(item.ID)
+ isoChar := strings.ToUpper(strings.TrimSpace(item.ISOChar))
+ isoNum := strings.TrimSpace(item.ISONum)
+ name := strings.TrimSpace(item.Name)
+ engName := strings.TrimSpace(item.EngName)
+ nominal, err := parseNominal(item.NominalStr)
+ if err != nil {
+ return nil, merrors.InvalidDataType("cbr: parse directory nominal: " + err.Error())
+ }
+ if id == "" || isoChar == "" {
+ return nil, merrors.InvalidDataType("cbr: directory contains entry with empty id or iso code")
+ }
+
+ info := valuteInfo{
+ ID: id,
+ ISOCharCode: isoChar,
+ ISONumCode: isoNum,
+ Name: name,
+ EngName: engName,
+ Nominal: nominal,
+ }
+
+ if existing, ok := byISO[isoChar]; ok && existing.ID != id {
+ return nil, merrors.InvalidDataType("cbr: duplicate ISO code " + isoChar)
+ }
+ if existing, ok := byID[id]; ok && existing.ISOCharCode != isoChar {
+ return nil, merrors.InvalidDataType("cbr: duplicate valute id " + id)
+ }
+ if isoNum != "" {
+ if existingID, ok := byNum[isoNum]; ok && existingID != id {
+ return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum)
+ }
+ byNum[isoNum] = id
+ }
+
+ byISO[isoChar] = info
+ byID[id] = info
+ }
+
+ if len(byISO) == 0 {
+ return nil, merrors.InvalidDataType("cbr: empty directory received")
+ }
+
+ return &valuteMapping{
+ byISO: byISO,
+ byID: byID,
+ }, nil
+}
+
+type dailyRates struct {
+ Valutes []dailyValute `xml:"Valute"`
+}
+
+type dailyValute struct {
+ ID string `xml:"ID,attr"`
+ NumCode string `xml:"NumCode"`
+ CharCode string `xml:"CharCode"`
+ Nominal string `xml:"Nominal"`
+ Name string `xml:"Name"`
+ Value string `xml:"Value"`
+}
+
+func (d *dailyRates) find(id string) *dailyValute {
+ if d == nil {
+ return nil
+ }
+ for idx := range d.Valutes {
+ if strings.EqualFold(strings.TrimSpace(d.Valutes[idx].ID), id) {
+ return &d.Valutes[idx]
+ }
+ }
+ return nil
+}
+
+type dynamicRates struct {
+ Records []dynamicRecord `xml:"Record"`
+}
+
+type dynamicRecord struct {
+ ID string `xml:"Id,attr"`
+ DateRaw string `xml:"Date,attr"`
+ Nominal string `xml:"Nominal"`
+ Value string `xml:"Value"`
+}
+
+func (d *dynamicRates) find(id string, date time.Time) *dynamicRecord {
+ if d == nil {
+ return nil
+ }
+ target := date.Format("02.01.2006")
+ for idx := range d.Records {
+ rec := &d.Records[idx]
+ if !strings.EqualFold(strings.TrimSpace(rec.ID), id) {
+ continue
+ }
+ if strings.TrimSpace(rec.DateRaw) == target {
+ return rec
+ }
+ }
+ return nil
+}
+
+func validateDailyEntry(expected valuteInfo, entry *dailyValute) error {
+ if entry == nil {
+ return merrors.NoData("cbr: missing daily entry")
+ }
+ if !strings.EqualFold(strings.TrimSpace(entry.CharCode), expected.ISOCharCode) {
+ return merrors.Internal("cbr: char code mismatch for " + expected.ISOCharCode)
+ }
+ if expected.ISONumCode != "" && strings.TrimSpace(entry.NumCode) != expected.ISONumCode {
+ return merrors.Internal("cbr: iso numeric mismatch for " + expected.ISOCharCode)
+ }
+ if expected.Name != "" && strings.TrimSpace(entry.Name) != expected.Name {
+ return merrors.Internal("cbr: currency name mismatch for " + expected.ISOCharCode)
+ }
+
+ nominal, err := parseNominal(entry.Nominal)
+ if err != nil {
+ return merrors.InvalidDataType("cbr: parse daily nominal: " + err.Error())
+ }
+ if nominal != expected.Nominal {
+ return merrors.Internal("cbr: nominal mismatch for " + expected.ISOCharCode)
+ }
+ return nil
+}
+
+func parseSymbol(symbol string) (string, *time.Time, error) {
+ trimmed := strings.TrimSpace(symbol)
+ if trimmed == "" {
+ return "", nil, merrors.InvalidArgument("cbr: symbol is empty", "symbol")
+ }
+
+ parts := strings.Split(trimmed, "@")
+ if len(parts) > 2 {
+ return "", nil, merrors.InvalidArgument("cbr: invalid symbol format", "symbol")
+ }
+
+ iso := strings.ToUpper(strings.TrimSpace(parts[0]))
+ if len(iso) != 3 {
+ return "", nil, merrors.InvalidArgument("cbr: symbol must be ISO currency code", "symbol")
+ }
+
+ if len(parts) == 1 {
+ return iso, nil, nil
+ }
+
+ datePart := strings.TrimSpace(parts[1])
+ if datePart == "" {
+ return "", nil, merrors.InvalidArgument("cbr: date component is empty", "symbol")
+ }
+
+ parsed, err := time.Parse("2006-01-02", datePart)
+ if err != nil {
+ return "", nil, merrors.InvalidArgumentWrap(err, "cbr: invalid date component", "symbol")
+ }
+
+ return iso, &parsed, nil
+}
+
+func parseNominal(value string) (int64, error) {
+ nominal, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
+ if err != nil || nominal <= 0 {
+ return 0, merrors.InvalidDataType("cbr: invalid nominal \"" + value + "\"")
+ }
+ return nominal, nil
+}
+
+func computePrice(value string, nominalStr string) (string, error) {
+ raw := strings.ReplaceAll(strings.TrimSpace(value), " ", "")
+ raw = strings.ReplaceAll(raw, ",", ".")
+
+ r := new(big.Rat)
+ if _, ok := r.SetString(raw); !ok {
+ return "", merrors.InvalidDataType("invalid decimal \"" + value + "\"")
+ }
+
+ nominal, err := parseNominal(nominalStr)
+ if err != nil {
+ return "", err
+ }
+
+ den := big.NewRat(nominal, 1)
+ price := new(big.Rat).Quo(r, den)
+ return price.FloatString(8), nil
+}
+
+func formatSymbol(iso string, asOf *time.Time) string {
+ if asOf == nil {
+ return iso
+ }
+ return iso + "@" + asOf.Format("2006-01-02")
+}
diff --git a/api/fx/ingestor/internal/market/cbr/connector_test.go b/api/fx/ingestor/internal/market/cbr/connector_test.go
new file mode 100644
index 0000000..d57cc9b
--- /dev/null
+++ b/api/fx/ingestor/internal/market/cbr/connector_test.go
@@ -0,0 +1,226 @@
+package cbr
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/tech/sendico/pkg/merrors"
+ "go.uber.org/zap"
+)
+
+func TestFetchTickerDaily(t *testing.T) {
+ transport := &stubRoundTripper{
+ responses: map[string]stubResponse{
+ "/scripts/XML_valFull.asp": {body: valuteDirectoryXML},
+ "/scripts/XML_daily.asp": {body: dailyRatesXML},
+ },
+ }
+
+ conn, err := NewConnector(zap.NewNop(), map[string]any{
+ "base_url": "http://cbr.test",
+ "http_round_tripper": transport,
+ "request_timeout_seconds": 1,
+ })
+ if err != nil {
+ t.Fatalf("NewConnector returned error: %v", err)
+ }
+
+ ticker, err := conn.FetchTicker(context.Background(), "USD")
+ if err != nil {
+ t.Fatalf("FetchTicker returned error: %v", err)
+ }
+
+ if ticker.Provider != "cbr" {
+ t.Fatalf("unexpected provider: %s", ticker.Provider)
+ }
+ if ticker.BidPrice != "95.12340000" || ticker.AskPrice != "95.12340000" {
+ t.Fatalf("unexpected bid/ask: %s / %s", ticker.BidPrice, ticker.AskPrice)
+ }
+ if ticker.Symbol != "USD" {
+ t.Fatalf("unexpected symbol: %s", ticker.Symbol)
+ }
+}
+
+func TestFetchTickerValidatesDailyEntry(t *testing.T) {
+ transport := &stubRoundTripper{
+ responses: map[string]stubResponse{
+ "/scripts/XML_valFull.asp": {body: valuteDirectoryXML},
+ "/scripts/XML_daily.asp": {body: strings.ReplaceAll(dailyRatesXML, "USD", "XXX")},
+ },
+ }
+
+ conn, err := NewConnector(zap.NewNop(), map[string]any{
+ "base_url": "http://cbr.test",
+ "http_round_tripper": transport,
+ })
+ if err != nil {
+ t.Fatalf("NewConnector returned error: %v", err)
+ }
+
+ if _, err := conn.FetchTicker(context.Background(), "USD"); err == nil {
+ t.Fatalf("FetchTicker expected to fail due to mismatch")
+ }
+}
+
+func TestFetchTickerHistorical(t *testing.T) {
+ transport := &stubRoundTripper{
+ responses: map[string]stubResponse{
+ "/scripts/XML_valFull.asp": {body: valuteDirectoryXML},
+ "/scripts/XML_dynamic.asp": {
+ body: dynamicRatesXML,
+ check: func(r *http.Request) error {
+ if got := r.URL.Query().Get("VAL_NM_RQ"); got != "R01235" {
+ return fmt.Errorf("unexpected valute id: %s", got)
+ }
+ if got := r.URL.Query().Get("date_req1"); got != "05/01/2023" {
+ return fmt.Errorf("unexpected date_req1: %s", got)
+ }
+ if got := r.URL.Query().Get("date_req2"); got != "05/01/2023" {
+ return fmt.Errorf("unexpected date_req2: %s", got)
+ }
+ return nil
+ },
+ },
+ },
+ }
+
+ conn, err := NewConnector(zap.NewNop(), map[string]any{
+ "base_url": "http://cbr.test",
+ "http_round_tripper": transport,
+ })
+ if err != nil {
+ t.Fatalf("NewConnector returned error: %v", err)
+ }
+
+ ticker, err := conn.FetchTicker(context.Background(), "USD@2023-01-05")
+ if err != nil {
+ t.Fatalf("FetchTicker returned error: %v", err)
+ }
+
+ if ticker.BidPrice != "70.10000000" || ticker.AskPrice != "70.10000000" {
+ t.Fatalf("unexpected bid/ask: %s / %s", ticker.BidPrice, ticker.AskPrice)
+ }
+ if ticker.Symbol != "USD@2023-01-05" {
+ t.Fatalf("unexpected symbol: %s", ticker.Symbol)
+ }
+}
+
+func TestFetchTickerUnknownCurrency(t *testing.T) {
+ transport := &stubRoundTripper{
+ responses: map[string]stubResponse{
+ "/scripts/XML_valFull.asp": {body: valuteDirectoryXML},
+ "/scripts/XML_daily.asp": {body: dailyRatesXML},
+ },
+ }
+
+ conn, err := NewConnector(zap.NewNop(), map[string]any{
+ "base_url": "http://cbr.test",
+ "http_round_tripper": transport,
+ })
+ if err != nil {
+ t.Fatalf("NewConnector returned error: %v", err)
+ }
+
+ _, err = conn.FetchTicker(context.Background(), "ZZZ")
+ if err == nil {
+ t.Fatalf("FetchTicker expected to fail for unknown currency")
+ }
+ if !errors.Is(err, merrors.ErrInvalidArg) {
+ t.Fatalf("expected invalid argument error, got %v", err)
+ }
+}
+
+func TestFetchTickerRespectsCustomPaths(t *testing.T) {
+ transport := &stubRoundTripper{
+ responses: map[string]stubResponse{
+ "/dir.xml": {body: valuteDirectoryXML},
+ "/rates.xml": {body: dailyRatesXML},
+ },
+ }
+
+ conn, err := NewConnector(zap.NewNop(), map[string]any{
+ "base_url": "http://cbr.test",
+ "directory_path": "/dir.xml",
+ "daily_path": "/rates.xml",
+ "http_round_tripper": transport,
+ })
+ if err != nil {
+ t.Fatalf("NewConnector returned error: %v", err)
+ }
+
+ if _, err := conn.FetchTicker(context.Background(), "USD"); err != nil {
+ t.Fatalf("FetchTicker returned error with custom paths: %v", err)
+ }
+}
+
+const valuteDirectoryXML = `
+
+ -
+ 840
+ USD
+ 1
+ US Dollar
+ US Dollar
+
+`
+
+const dailyRatesXML = `
+
+
+ 840
+ USD
+ 1
+ US Dollar
+ 95,1234
+
+`
+
+const dynamicRatesXML = `
+
+
+ 1
+ 70,1
+
+`
+
+type stubResponse struct {
+ status int
+ body string
+ check func(*http.Request) error
+}
+
+type stubRoundTripper struct {
+ responses map[string]stubResponse
+}
+
+func (s *stubRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ if s.responses == nil {
+ return nil, fmt.Errorf("no responses configured")
+ }
+ res, ok := s.responses[req.URL.Path]
+ if !ok {
+ return nil, fmt.Errorf("unexpected request path: %s", req.URL.Path)
+ }
+ if res.check != nil {
+ if err := res.check(req); err != nil {
+ return nil, err
+ }
+ }
+
+ status := res.status
+ if status == 0 {
+ status = http.StatusOK
+ }
+
+ return &http.Response{
+ StatusCode: status,
+ Body: io.NopCloser(strings.NewReader(res.body)),
+ Header: http.Header{"Content-Type": []string{"text/xml"}},
+ Request: req,
+ }, nil
+}
diff --git a/api/fx/ingestor/internal/market/coingecko/connector.go b/api/fx/ingestor/internal/market/coingecko/connector.go
index 9b878f5..36ecb46 100644
--- a/api/fx/ingestor/internal/market/coingecko/connector.go
+++ b/api/fx/ingestor/internal/market/coingecko/connector.go
@@ -10,9 +10,9 @@ import (
"strings"
"time"
- "github.com/tech/sendico/fx/ingestor/internal/fmerrors"
"github.com/tech/sendico/fx/ingestor/internal/market/common"
mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
+ "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
@@ -61,7 +61,7 @@ func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Conne
parsed, err := url.Parse(baseURL)
if err != nil {
- return nil, fmerrors.Wrap("coingecko: invalid base url", err)
+ return nil, merrors.InvalidArgumentWrap(err, "coingecko: invalid base url", "base_url")
}
transport := &http.Transport{
@@ -96,7 +96,7 @@ func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*m
endpoint, err := url.Parse(c.base)
if err != nil {
- return nil, fmerrors.Wrap("coingecko: parse base url", err)
+ return nil, merrors.InternalWrap(err, "coingecko: parse base url")
}
endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/simple/price"
query := endpoint.Query()
@@ -107,19 +107,19 @@ func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*m
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
- return nil, fmerrors.Wrap("coingecko: build request", err)
+ return nil, merrors.InternalWrap(err, "coingecko: build request")
}
resp, err := c.client.Do(req)
if err != nil {
c.logger.Warn("CoinGecko request failed", zap.String("symbol", symbol), zap.Error(err))
- return nil, fmerrors.Wrap("coingecko: request failed", err)
+ return nil, merrors.InternalWrap(err, "coingecko: request failed")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.logger.Warn("CoinGecko returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode))
- return nil, fmerrors.New("coingecko: unexpected status " + strconv.Itoa(resp.StatusCode))
+ return nil, merrors.Internal("coingecko: unexpected status " + strconv.Itoa(resp.StatusCode))
}
decoder := json.NewDecoder(resp.Body)
@@ -128,21 +128,21 @@ func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*m
var payload map[string]map[string]interface{}
if err := decoder.Decode(&payload); err != nil {
c.logger.Warn("CoinGecko decode failed", zap.String("symbol", symbol), zap.Error(err))
- return nil, fmerrors.Wrap("coingecko: decode response", err)
+ return nil, merrors.InternalWrap(err, "coingecko: decode response")
}
coinData, ok := payload[coinID]
if !ok {
- return nil, fmerrors.New("coingecko: coin id not found in response")
+ return nil, merrors.Internal("coingecko: coin id not found in response")
}
priceValue, ok := coinData[vsCurrency]
if !ok {
- return nil, fmerrors.New("coingecko: vs currency not found in response")
+ return nil, merrors.Internal("coingecko: vs currency not found in response")
}
price, ok := toFloat(priceValue)
if !ok || price <= 0 {
- return nil, fmerrors.New("coingecko: invalid price value in response")
+ return nil, merrors.Internal("coingecko: invalid price value in response")
}
priceStr := strconv.FormatFloat(price, 'f', -1, 64)
@@ -171,7 +171,7 @@ func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*m
func parseSymbol(symbol string) (string, string, error) {
trimmed := strings.TrimSpace(symbol)
if trimmed == "" {
- return "", "", fmerrors.New("coingecko: symbol is empty")
+ return "", "", merrors.InvalidArgument("coingecko: symbol is empty", "symbol")
}
parts := strings.FieldsFunc(strings.ToLower(trimmed), func(r rune) bool {
@@ -183,13 +183,13 @@ func parseSymbol(symbol string) (string, string, error) {
})
if len(parts) != 2 {
- return "", "", fmerrors.New("coingecko: symbol must be /")
+ return "", "", merrors.InvalidArgument("coingecko: symbol must be /", "symbol")
}
coinID := strings.TrimSpace(parts[0])
vsCurrency := strings.TrimSpace(parts[1])
if coinID == "" || vsCurrency == "" {
- return "", "", fmerrors.New("coingecko: symbol contains empty segments")
+ return "", "", merrors.InvalidArgument("coingecko: symbol contains empty segments", "symbol")
}
return coinID, vsCurrency, nil
diff --git a/api/fx/ingestor/internal/market/factory.go b/api/fx/ingestor/internal/market/factory.go
index e2d0a5f..86f6034 100644
--- a/api/fx/ingestor/internal/market/factory.go
+++ b/api/fx/ingestor/internal/market/factory.go
@@ -5,10 +5,11 @@ import (
"strings"
"time"
- "github.com/tech/sendico/fx/ingestor/internal/fmerrors"
"github.com/tech/sendico/fx/ingestor/internal/market/binance"
+ "github.com/tech/sendico/fx/ingestor/internal/market/cbr"
"github.com/tech/sendico/fx/ingestor/internal/market/coingecko"
mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
+ "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
@@ -21,7 +22,7 @@ func BuildConnectors(logger mlogger.Logger, configs []model.DriverConfig[mmodel.
for _, cfg := range configs {
driver := mmodel.NormalizeDriver(cfg.Driver)
if driver.IsEmpty() {
- return nil, fmerrors.New("market: connector driver is empty")
+ return nil, merrors.InvalidArgument("market: connector driver is empty", "driver")
}
var (
@@ -34,12 +35,14 @@ func BuildConnectors(logger mlogger.Logger, configs []model.DriverConfig[mmodel.
conn, err = binance.NewConnector(logger, cfg.Settings)
case mmodel.DriverCoinGecko:
conn, err = coingecko.NewConnector(logger, cfg.Settings)
+ case mmodel.DriverCBR:
+ conn, err = cbr.NewConnector(logger, cfg.Settings)
default:
- err = fmerrors.New("market: unsupported driver " + driver.String())
+ err = merrors.InvalidArgument("market: unsupported driver "+driver.String(), "driver")
}
if err != nil {
- return nil, fmerrors.Wrap("market: build connector "+driver.String(), err)
+ return nil, merrors.InternalWrap(err, "market: build connector "+driver.String())
}
connectors[driver] = conn
}
diff --git a/api/fx/ingestor/internal/metrics/server.go b/api/fx/ingestor/internal/metrics/server.go
index b7405cf..12f04c5 100644
--- a/api/fx/ingestor/internal/metrics/server.go
+++ b/api/fx/ingestor/internal/metrics/server.go
@@ -8,12 +8,12 @@ import (
"time"
"github.com/go-chi/chi/v5"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/tech/sendico/fx/ingestor/internal/config"
- "github.com/tech/sendico/fx/ingestor/internal/fmerrors"
"github.com/tech/sendico/pkg/api/routers"
"github.com/tech/sendico/pkg/api/routers/health"
+ "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
- "github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
)
@@ -30,7 +30,7 @@ type Server interface {
func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error) {
if logger == nil {
- return nil, fmerrors.New("metrics: logger is nil")
+ return nil, merrors.InvalidArgument("metrics: logger is nil")
}
if cfg == nil || !cfg.Enabled {
logger.Debug("Metrics disabled; using noop server")
diff --git a/api/fx/ingestor/internal/model/connector.go b/api/fx/ingestor/internal/model/connector.go
index 7dc4f9c..7fff1f6 100644
--- a/api/fx/ingestor/internal/model/connector.go
+++ b/api/fx/ingestor/internal/model/connector.go
@@ -10,6 +10,7 @@ type Driver string
const (
DriverBinance Driver = "BINANCE"
DriverCoinGecko Driver = "COINGECKO"
+ DriverCBR Driver = "CBR"
)
func (d Driver) String() string {
diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod
index 5089e0a..64b70ba 100644
--- a/api/fx/oracle/go.mod
+++ b/api/fx/oracle/go.mod
@@ -47,8 +47,8 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
)
diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum
index d63f35a..4d80caf 100644
--- a/api/fx/oracle/go.sum
+++ b/api/fx/oracle/go.sum
@@ -187,24 +187,24 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/api/fx/storage/go.mod b/api/fx/storage/go.mod
index 536b0a3..8027b12 100644
--- a/api/fx/storage/go.mod
+++ b/api/fx/storage/go.mod
@@ -26,7 +26,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
diff --git a/api/fx/storage/go.sum b/api/fx/storage/go.sum
index 2334065..eee8330 100644
--- a/api/fx/storage/go.sum
+++ b/api/fx/storage/go.sum
@@ -147,8 +147,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -162,8 +162,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/api/gateway/chain/go.mod b/api/gateway/chain/go.mod
index ffb3ce9..a295b32 100644
--- a/api/gateway/chain/go.mod
+++ b/api/gateway/chain/go.mod
@@ -22,7 +22,7 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
- github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7 // indirect
+ github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251208031133-be43a854e4be // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
@@ -82,9 +82,9 @@ require (
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
)
diff --git a/api/gateway/chain/go.sum b/api/gateway/chain/go.sum
index b02c189..5a9570f 100644
--- a/api/gateway/chain/go.sum
+++ b/api/gateway/chain/go.sum
@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7 h1:uups37roJCTtR/BrJa0WoMrxt3rzgV+Qrj+TxYyJoAo=
-github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
+github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251208031133-be43a854e4be h1:1LtMLkGIqE5IQZ7Vdh4zv8A6LECInKF86/fTVxKxYLE=
+github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251208031133-be43a854e4be/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -333,8 +333,8 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -343,16 +343,16 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/api/gateway/mntx/go.mod b/api/gateway/mntx/go.mod
index 6fc7a2a..4aecbf3 100644
--- a/api/gateway/mntx/go.mod
+++ b/api/gateway/mntx/go.mod
@@ -47,8 +47,8 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
)
diff --git a/api/gateway/mntx/go.sum b/api/gateway/mntx/go.sum
index 79911b6..5b881ba 100644
--- a/api/gateway/mntx/go.sum
+++ b/api/gateway/mntx/go.sum
@@ -189,24 +189,24 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/api/ledger/go.mod b/api/ledger/go.mod
index 6864509..24ab43e 100644
--- a/api/ledger/go.mod
+++ b/api/ledger/go.mod
@@ -48,8 +48,8 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
)
diff --git a/api/ledger/go.sum b/api/ledger/go.sum
index 1f5a08d..51e42ef 100644
--- a/api/ledger/go.sum
+++ b/api/ledger/go.sum
@@ -189,24 +189,24 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/api/notification/go.mod b/api/notification/go.mod
index 4f2032f..e89ca0b 100644
--- a/api/notification/go.mod
+++ b/api/notification/go.mod
@@ -14,7 +14,7 @@ require (
github.com/xhit/go-simple-mail/v2 v2.16.0
go.mongodb.org/mongo-driver v1.17.6
go.uber.org/zap v1.27.1
- golang.org/x/text v0.31.0
+ golang.org/x/text v0.32.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -50,8 +50,8 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
diff --git a/api/notification/go.sum b/api/notification/go.sum
index d018b97..6d7f489 100644
--- a/api/notification/go.sum
+++ b/api/notification/go.sum
@@ -202,24 +202,24 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod
index f36be07..55a9b77 100644
--- a/api/payments/orchestrator/go.mod
+++ b/api/payments/orchestrator/go.mod
@@ -56,8 +56,8 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
)
diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum
index e9646dc..f9d3e69 100644
--- a/api/payments/orchestrator/go.sum
+++ b/api/payments/orchestrator/go.sum
@@ -190,24 +190,24 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/api/pkg/go.mod b/api/pkg/go.mod
index dde3a93..ebd34f4 100644
--- a/api/pkg/go.mod
+++ b/api/pkg/go.mod
@@ -89,9 +89,9 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/api/pkg/go.sum b/api/pkg/go.sum
index 7ed16f1..d705953 100644
--- a/api/pkg/go.sum
+++ b/api/pkg/go.sum
@@ -223,8 +223,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -240,8 +240,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
@@ -250,8 +250,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/api/server/go.mod b/api/server/go.mod
index e9c07f7..f612b60 100644
--- a/api/server/go.mod
+++ b/api/server/go.mod
@@ -132,9 +132,9 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/text v0.31.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
)
diff --git a/api/server/go.sum b/api/server/go.sum
index 1500397..1adc889 100644
--- a/api/server/go.sum
+++ b/api/server/go.sum
@@ -311,8 +311,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -328,8 +328,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
@@ -338,8 +338,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=