diff --git a/api/.DS_Store b/api/.DS_Store deleted file mode 100644 index eace314..0000000 Binary files a/api/.DS_Store and /dev/null differ diff --git a/api/fx/oracle/client/client.go b/api/fx/oracle/client/client.go index 82ab2d2..ca8b6fd 100644 --- a/api/fx/oracle/client/client.go +++ b/api/fx/oracle/client/client.go @@ -3,11 +3,11 @@ package client import ( "context" "crypto/tls" - "errors" "fmt" "strings" "time" + "github.com/tech/sendico/pkg/merrors" fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" @@ -88,7 +88,7 @@ type oracleClient struct { func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { cfg.setDefaults() if strings.TrimSpace(cfg.Address) == "" { - return nil, errors.New("oracle: address is required") + return nil, merrors.InvalidArgument("oracle: address is required") } dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) @@ -105,7 +105,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) if err != nil { - return nil, fmt.Errorf("oracle: dial %s: %w", cfg.Address, err) + return nil, merrors.InternalWrap(err, fmt.Sprintf("oracle: dial %s", cfg.Address)) } return &oracleClient{ @@ -133,7 +133,7 @@ func (c *oracleClient) Close() error { func (c *oracleClient) LatestRate(ctx context.Context, req LatestRateParams) (*RateSnapshot, error) { if req.Pair == nil { - return nil, errors.New("oracle: pair is required") + return nil, merrors.InvalidArgument("oracle: pair is required") } callCtx, cancel := c.callContext(ctx) @@ -145,26 +145,26 @@ func (c *oracleClient) LatestRate(ctx context.Context, req LatestRateParams) (*R Provider: req.Provider, }) if err != nil { - return nil, fmt.Errorf("oracle: latest rate: %w", err) + return nil, merrors.InternalWrap(err, "oracle: latest rate") } if resp.GetRate() == nil { - return nil, errors.New("oracle: latest rate: empty payload") + return nil, merrors.Internal("oracle: latest rate: empty payload") } return fromProtoRate(resp.GetRate()), nil } func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote, error) { if req.Pair == nil { - return nil, errors.New("oracle: pair is required") + return nil, merrors.InvalidArgument("oracle: pair is required") } if req.Side == fxv1.Side_SIDE_UNSPECIFIED { - return nil, errors.New("oracle: side is required") + return nil, merrors.InvalidArgument("oracle: side is required") } baseSupplied := req.BaseAmount != nil quoteSupplied := req.QuoteAmount != nil if baseSupplied == quoteSupplied { - return nil, errors.New("oracle: exactly one of base_amount or quote_amount must be set") + return nil, merrors.InvalidArgument("oracle: exactly one of base_amount or quote_amount must be set") } callCtx, cancel := c.callContext(ctx) @@ -191,10 +191,10 @@ func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote resp, err := c.client.GetQuote(callCtx, protoReq) if err != nil { - return nil, fmt.Errorf("oracle: get quote: %w", err) + return nil, merrors.InternalWrap(err, "oracle: get quote") } if resp.GetQuote() == nil { - return nil, errors.New("oracle: get quote: empty payload") + return nil, merrors.Internal("oracle: get quote: empty payload") } return fromProtoQuote(resp.GetQuote()), nil } diff --git a/api/ledger/client/client.go b/api/ledger/client/client.go index f71927c..3b0671a 100644 --- a/api/ledger/client/client.go +++ b/api/ledger/client/client.go @@ -3,11 +3,11 @@ package client import ( "context" "crypto/tls" - "errors" "fmt" "strings" "time" + "github.com/tech/sendico/pkg/merrors" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -48,7 +48,7 @@ type ledgerClient struct { func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { cfg.setDefaults() if strings.TrimSpace(cfg.Address) == "" { - return nil, errors.New("ledger: address is required") + return nil, merrors.InvalidArgument("ledger: address is required") } dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) @@ -65,7 +65,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) if err != nil { - return nil, fmt.Errorf("ledger: dial %s: %w", cfg.Address, err) + return nil, merrors.InternalWrap(err, fmt.Sprintf("ledger: dial %s", cfg.Address)) } return &ledgerClient{ diff --git a/api/ledger/internal/service/ledger/helpers.go b/api/ledger/internal/service/ledger/helpers.go index 63baf19..de0b9b8 100644 --- a/api/ledger/internal/service/ledger/helpers.go +++ b/api/ledger/internal/service/ledger/helpers.go @@ -146,7 +146,7 @@ func calculateBalance(lines []*model.PostingLine) (decimal.Decimal, error) { for _, line := range lines { amount, err := parseDecimal(line.Amount) if err != nil { - return decimal.Zero, fmt.Errorf("invalid line amount: %w", err) + return decimal.Zero, merrors.InvalidArgumentWrap(err, "invalid line amount") } balance = balance.Add(amount) } diff --git a/api/ledger/internal/service/ledger/outbox_publisher.go b/api/ledger/internal/service/ledger/outbox_publisher.go index 30a3a3d..bad4baf 100644 --- a/api/ledger/internal/service/ledger/outbox_publisher.go +++ b/api/ledger/internal/service/ledger/outbox_publisher.go @@ -8,6 +8,7 @@ import ( "github.com/tech/sendico/ledger/storage" ledgerModel "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/merrors" pmessaging "github.com/tech/sendico/pkg/messaging" me "github.com/tech/sendico/pkg/messaging/envelope" "github.com/tech/sendico/pkg/mlogger" @@ -126,7 +127,7 @@ func (p *outboxPublisher) dispatchPending(ctx context.Context) (int, error) { func (p *outboxPublisher) publishEvent(_ context.Context, event *ledgerModel.OutboxEvent) error { docID := event.GetID() if docID == nil || docID.IsZero() { - return errors.New("outbox event missing identifier") + return merrors.InvalidArgument("outbox event missing identifier") } payload, err := p.wrapPayload(event) @@ -157,7 +158,7 @@ func (p *outboxPublisher) wrapPayload(event *ledgerModel.OutboxEvent) ([]byte, e func (p *outboxPublisher) markSent(ctx context.Context, event *ledgerModel.OutboxEvent) error { eventRef := event.GetID() if eventRef == nil || eventRef.IsZero() { - return errors.New("outbox event missing identifier") + return merrors.InvalidArgument("outbox event missing identifier") } return p.store.MarkSent(ctx, *eventRef, time.Now().UTC()) diff --git a/api/ledger/internal/service/ledger/queries.go b/api/ledger/internal/service/ledger/queries.go index e59e8dd..a61ea9f 100644 --- a/api/ledger/internal/service/ledger/queries.go +++ b/api/ledger/internal/service/ledger/queries.go @@ -249,15 +249,15 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat func parseCursor(cursor string) (int, error) { decoded, err := base64.StdEncoding.DecodeString(cursor) if err != nil { - return 0, fmt.Errorf("invalid base64: %w", err) + return 0, merrors.InvalidArgumentWrap(err, "invalid cursor base64 encoding") } parts := strings.Split(string(decoded), ":") if len(parts) != 2 || parts[0] != "offset" { - return 0, fmt.Errorf("invalid cursor format") + return 0, merrors.InvalidArgument("invalid cursor format") } offset, err := strconv.Atoi(parts[1]) if err != nil { - return 0, fmt.Errorf("invalid offset: %w", err) + return 0, merrors.InvalidArgumentWrap(err, "invalid cursor offset") } return offset, nil } diff --git a/api/ledger/internal/service/ledger/service.go b/api/ledger/internal/service/ledger/service.go index ce9c4cb..3763481 100644 --- a/api/ledger/internal/service/ledger/service.go +++ b/api/ledger/internal/service/ledger/service.go @@ -18,6 +18,7 @@ import ( "github.com/tech/sendico/ledger/storage" "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/merrors" pmessaging "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" @@ -241,10 +242,10 @@ func (s *Service) quoteFees(ctx context.Context, trigger feesv1.Trigger, organiz return nil, nil } if strings.TrimSpace(organizationRef) == "" { - return nil, fmt.Errorf("organization reference is required to quote fees") + return nil, merrors.InvalidArgument("organization reference is required to quote fees") } if baseAmount == nil { - return nil, fmt.Errorf("base amount is required to quote fees") + return nil, merrors.InvalidArgument("base amount is required to quote fees") } amountCopy := &moneyv1.Money{Amount: baseAmount.GetAmount(), Currency: baseAmount.GetCurrency()} @@ -309,11 +310,11 @@ func convertFeeDerivedLines(lines []*feesv1.DerivedPostingLine) ([]*ledgerv1.Pos continue } if line.GetMoney() == nil { - return nil, fmt.Errorf("fee line %d missing money", idx) + return nil, merrors.Internal(fmt.Sprintf("fee line %d missing money", idx)) } dec, err := decimal.NewFromString(line.GetMoney().GetAmount()) if err != nil { - return nil, fmt.Errorf("fee line %d invalid amount: %w", idx, err) + return nil, merrors.InternalWrap(err, fmt.Sprintf("fee line %d invalid amount", idx)) } dec = ensureAmountForSide(dec, line.GetSide()) posting := &ledgerv1.PostingLine{ diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index 35913b4..c5d32c3 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -2,12 +2,12 @@ package notificationimp import ( "context" - "fmt" "github.com/tech/sendico/notification/interface/api" mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail" "github.com/tech/sendico/notification/internal/server/notificationimp/telegram" "github.com/tech/sendico/pkg/domainprovider" + "github.com/tech/sendico/pkg/merrors" na "github.com/tech/sendico/pkg/messaging/notifications/account" ni "github.com/tech/sendico/pkg/messaging/notifications/invitation" snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" @@ -39,10 +39,10 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { p.logger = a.Logger().Named(p.Name()) if a.Config().Notification == nil { - return nil, fmt.Errorf("notification configuration is missing") + return nil, merrors.InvalidArgument("notification configuration is missing") } if a.Config().Notification.Telegram == nil { - return nil, fmt.Errorf("telegram configuration is missing") + return nil, merrors.InvalidArgument("telegram configuration is missing") } var err error @@ -90,7 +90,7 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { func (a *NotificationAPI) onDemoRequest(ctx context.Context, request *model.DemoRequest) error { if a.tg == nil { - return fmt.Errorf("telegram client is not configured") + return merrors.Internal("telegram client is not configured") } if err := a.tg.SendDemoRequest(ctx, request); err != nil { a.logger.Warn("Failed to send demo request via telegram", zap.Error(err)) diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index d91c7b9..030264c 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -13,6 +13,7 @@ import ( "time" notconfig "github.com/tech/sendico/notification/interface/services/notification/config" + "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" ) @@ -45,15 +46,15 @@ type sendMessagePayload struct { func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, error) { if cfg == nil { - return nil, fmt.Errorf("telegram configuration is not provided") + return nil, merrors.InvalidArgument("telegram configuration is not provided") } token := strings.TrimSpace(os.Getenv(cfg.BotTokenEnv)) if token == "" { - return nil, fmt.Errorf("telegram bot token env %s is empty", cfg.BotTokenEnv) + return nil, merrors.InvalidArgument(fmt.Sprintf("telegram bot token env %s is empty", cfg.BotTokenEnv)) } chatID := strings.TrimSpace(os.Getenv(cfg.ChatIDEnv)) if chatID == "" { - return nil, fmt.Errorf("telegram chat id env %s is empty", cfg.ChatIDEnv) + return nil, merrors.InvalidArgument(fmt.Sprintf("telegram chat id env %s is empty", cfg.ChatIDEnv)) } var threadID *int64 @@ -62,7 +63,7 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er if raw != "" { val, err := strconv.ParseInt(raw, 10, 64) if err != nil { - return nil, fmt.Errorf("telegram thread id env %s is invalid: %w", env, err) + return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("telegram thread id env %s is invalid", env)) } threadID = &val } @@ -93,7 +94,7 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest) error { if request == nil { - return fmt.Errorf("demo request payload is nil") + return merrors.InvalidArgument("demo request payload is nil") } message := buildMessage(request) payload := sendMessagePayload{ @@ -127,7 +128,7 @@ func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) er return nil } respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10)) - return fmt.Errorf("telegram sendMessage failed with status %d: %s", resp.StatusCode, string(respBody)) + return merrors.Internal(fmt.Sprintf("telegram sendMessage failed with status %d: %s", resp.StatusCode, string(respBody))) } func (c *client) endpoint() string { diff --git a/api/payments/orchestrator/client/client.go b/api/payments/orchestrator/client/client.go index e98b3ee..c571933 100644 --- a/api/payments/orchestrator/client/client.go +++ b/api/payments/orchestrator/client/client.go @@ -3,11 +3,11 @@ package client import ( "context" "crypto/tls" - "errors" "fmt" "strings" "time" + "github.com/tech/sendico/pkg/merrors" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -48,7 +48,7 @@ type orchestratorClient struct { func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { cfg.setDefaults() if strings.TrimSpace(cfg.Address) == "" { - return nil, errors.New("payment-orchestrator: address is required") + return nil, merrors.InvalidArgument("payment-orchestrator: address is required") } dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) @@ -65,7 +65,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) if err != nil { - return nil, fmt.Errorf("payment-orchestrator: dial %s: %w", cfg.Address, err) + return nil, merrors.InternalWrap(err, fmt.Sprintf("payment-orchestrator: dial %s", cfg.Address)) } return &orchestratorClient{ diff --git a/api/pkg/merrors/errors.go b/api/pkg/merrors/errors.go index cb558c0..3667374 100644 --- a/api/pkg/merrors/errors.go +++ b/api/pkg/merrors/errors.go @@ -49,13 +49,13 @@ func AccessDenied(object, action string, objectRef primitive.ObjectID) error { var ErrInvalidDataType = errors.New("invalidDataType") func InvalidDataType(msg string) error { - return fmt.Errorf("%w: %s", ErrDataConflict, msg) + return fmt.Errorf("%w: %s", ErrInvalidDataType, msg) } var ErrUnauthorized = errors.New("unathorized") func Unauthorized(msg string) error { - return fmt.Errorf("%w: %s", ErrDataConflict, msg) + return fmt.Errorf("%w: %s", ErrUnauthorized, msg) } var ErrNoMessagingTopic = errors.New("messagingTopicError") @@ -63,3 +63,19 @@ var ErrNoMessagingTopic = errors.New("messagingTopicError") func NoMessagingTopic(topic string) error { return fmt.Errorf("%w: messaging topic '%s' not found", ErrNoMessagingTopic, topic) } + +func InvalidArgumentWrap(err error, msg string) error { + return wrapError(ErrInvalidArg, msg, err) +} + +func InternalWrap(err error, msg string) error { + return wrapError(ErrInternal, msg, err) +} + +func wrapError(base error, msg string, err error) error { + baseErr := fmt.Errorf("%w: %s", base, msg) + if err == nil { + return baseErr + } + return errors.Join(baseErr, err) +} diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index 0225c23..d40f824 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -1,9 +1,8 @@ package notifications import ( - "fmt" - gmessaging "github.com/tech/sendico/pkg/generated/gmessaging" + "github.com/tech/sendico/pkg/merrors" messaging "github.com/tech/sendico/pkg/messaging/envelope" "github.com/tech/sendico/pkg/model" nm "github.com/tech/sendico/pkg/model/notification" @@ -18,7 +17,7 @@ type DemoRequestNotification struct { func (drn *DemoRequestNotification) Serialize() ([]byte, error) { if drn.request == nil { - return nil, fmt.Errorf("demo request payload is empty") + return nil, merrors.InvalidArgument("demo request payload is empty") } msg := gmessaging.DemoRequestEvent{ Name: drn.request.Name, diff --git a/api/server/interface/api/config.go b/api/server/interface/api/config.go index c1f8850..f21e3af 100644 --- a/api/server/interface/api/config.go +++ b/api/server/interface/api/config.go @@ -6,12 +6,13 @@ import ( ) type Config struct { - Mw *mwa.Config `yaml:"middleware"` - Storage *fsc.Config `yaml:"storage"` + Mw *mwa.Config `yaml:"middleware"` + Storage *fsc.Config `yaml:"storage"` ChainGateway *ChainGatewayConfig `yaml:"chain_gateway"` } type ChainGatewayConfig struct { + Address string `yaml:"address"` AddressEnv string `yaml:"address_env"` DialTimeoutSeconds int `yaml:"dial_timeout_seconds"` CallTimeoutSeconds int `yaml:"call_timeout_seconds"` diff --git a/api/server/internal/server/accountapiimp/service.go b/api/server/internal/server/accountapiimp/service.go index 169d464..df3a0e8 100644 --- a/api/server/internal/server/accountapiimp/service.go +++ b/api/server/internal/server/accountapiimp/service.go @@ -16,6 +16,7 @@ import ( "github.com/tech/sendico/pkg/db/refreshtokens" "github.com/tech/sendico/pkg/db/transaction" "github.com/tech/sendico/pkg/domainprovider" + "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" @@ -142,7 +143,12 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) { } p.accountsPermissionRef = accountsPolicy.ID - if err := p.initChainGateway(a.Config()); err != nil { + cfg := a.Config() + if cfg == nil { + p.logger.Error("Failed to fetch service configuration") + return nil, merrors.InvalidArgument("No configuration provided") + } + if err := p.initChainGateway(cfg.ChainGateway); err != nil { p.logger.Error("Failed to initialize chain gateway client", zap.Error(err)) return nil, err } @@ -150,21 +156,21 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) { return p, nil } -func (a *AccountAPI) initChainGateway(cfg *eapi.Config) error { - if cfg == nil || cfg.ChainGateway == nil { - return fmt.Errorf("chain gateway configuration is not provided") +func (a *AccountAPI) initChainGateway(cfg *eapi.ChainGatewayConfig) error { + if cfg == nil { + return merrors.InvalidArgument("chain gateway configuration is not provided") } - address := strings.TrimSpace(os.Getenv(cfg.ChainGateway.AddressEnv)) + address := strings.TrimSpace(os.Getenv(cfg.AddressEnv)) if address == "" { - return fmt.Errorf("chain gateway address env %s is empty", cfg.ChainGateway.AddressEnv) + return merrors.InvalidArgument(fmt.Sprintf("chain gateway address env %s is empty", cfg.AddressEnv)) } clientCfg := chaingatewayclient.Config{ Address: address, - DialTimeout: time.Duration(cfg.ChainGateway.DialTimeoutSeconds) * time.Second, - CallTimeout: time.Duration(cfg.ChainGateway.CallTimeoutSeconds) * time.Second, - Insecure: cfg.ChainGateway.Insecure, + DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second, + CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second, + Insecure: cfg.Insecure, } client, err := chaingatewayclient.New(context.Background(), clientCfg) @@ -172,7 +178,7 @@ func (a *AccountAPI) initChainGateway(cfg *eapi.Config) error { return err } - asset, err := buildGatewayAsset(cfg.ChainGateway.DefaultAsset) + asset, err := buildGatewayAsset(cfg.DefaultAsset) if err != nil { _ = client.Close() return err @@ -190,7 +196,7 @@ func buildGatewayAsset(cfg eapi.ChainGatewayAssetConfig) (*gatewayv1.Asset, erro } tokenSymbol := strings.TrimSpace(cfg.TokenSymbol) if tokenSymbol == "" { - return nil, fmt.Errorf("chain gateway token symbol is required") + return nil, merrors.InvalidArgument("chain gateway token symbol is required") } return &gatewayv1.Asset{ Chain: chain, @@ -208,8 +214,8 @@ func parseChainNetwork(value string) (gatewayv1.ChainNetwork, error) { case "OTHER_EVM", "CHAIN_NETWORK_OTHER_EVM": return gatewayv1.ChainNetwork_CHAIN_NETWORK_OTHER_EVM, nil case "", "CHAIN_NETWORK_UNSPECIFIED": - return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, fmt.Errorf("chain network must be specified") + return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("chain network must be specified") default: - return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, fmt.Errorf("unsupported chain network %s", value) + return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("unsupported chain network %s", value) } }