From 605f0ba139a24a53786ae08fe5dcbde28c9e5876 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 27 Feb 2026 16:23:50 +0100 Subject: [PATCH] got rid of fees dependency in ledger --- api/ledger/client/client.go | 18 +- api/ledger/config.dev.yml | 4 - api/ledger/config.yml | 4 - .../internal/server/internal/serverimp.go | 51 +---- api/ledger/internal/service/ledger/posting.go | 7 - .../internal/service/ledger/posting_debit.go | 7 - .../service/ledger/posting_external.go | 14 -- api/ledger/internal/service/ledger/service.go | 212 +----------------- api/pkg/chain/asset.go | 2 +- api/pkg/chain/asset_test.go | 5 + api/server/go.sum | 4 +- .../internal/server/accountapiimp/service.go | 4 +- .../server/accountapiimp/service_test.go | 19 ++ 13 files changed, 51 insertions(+), 300 deletions(-) create mode 100644 api/server/internal/server/accountapiimp/service_test.go diff --git a/api/ledger/client/client.go b/api/ledger/client/client.go index 9bfe1713..58166d2c 100644 --- a/api/ledger/client/client.go +++ b/api/ledger/client/client.go @@ -860,7 +860,23 @@ func normalizeLedgerTxRails(tx rail.LedgerTx) rail.LedgerTx { } func normalizeRail(value string) string { - return discovery.NormalizeRail(value) + clean := discovery.NormalizeRail(value) + switch clean { + case "RAIL_CRYPTO": + return discovery.RailCrypto + case "PROVIDER_SETTLEMENT", "RAIL_SETTLEMENT", "RAIL_PROVIDER_SETTLEMENT": + return discovery.RailProviderSettlement + case "RAIL_LEDGER": + return discovery.RailLedger + case "CARD_PAYOUT", "RAIL_CARD", "RAIL_CARD_PAYOUT": + return discovery.RailCardPayout + case "FIAT_ONRAMP", "RAIL_ONRAMP", "RAIL_FIAT_ONRAMP": + return discovery.RailFiatOnRamp + case "FIAT_OFFRAMP", "RAIL_OFFRAMP", "RAIL_FIAT_OFFRAMP": + return discovery.RailFiatOffRamp + default: + return clean + } } func cloneMoney(input *moneyv1.Money) *moneyv1.Money { diff --git a/api/ledger/config.dev.yml b/api/ledger/config.dev.yml index 481e4960..0f8379cb 100644 --- a/api/ledger/config.dev.yml +++ b/api/ledger/config.dev.yml @@ -40,7 +40,3 @@ messaging: batch_size: 100 poll_interval_seconds: 1 max_attempts: 5 - -fees: - address: "dev-billing-fees:50060" - timeout_seconds: 3 diff --git a/api/ledger/config.yml b/api/ledger/config.yml index e2b5a11c..bbf2f8df 100644 --- a/api/ledger/config.yml +++ b/api/ledger/config.yml @@ -40,7 +40,3 @@ messaging: batch_size: 100 poll_interval_seconds: 1 max_attempts: 5 - -fees: - address: "sendico_billing_fees:50060" - timeout_seconds: 3 diff --git a/api/ledger/internal/server/internal/serverimp.go b/api/ledger/internal/server/internal/serverimp.go index 3213342f..d3ad0d6d 100644 --- a/api/ledger/internal/server/internal/serverimp.go +++ b/api/ledger/internal/server/internal/serverimp.go @@ -3,7 +3,6 @@ package serverimp import ( "context" "os" - "strings" "time" "github.com/tech/sendico/ledger/internal/service/ledger" @@ -13,11 +12,8 @@ import ( "github.com/tech/sendico/pkg/db" msg "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" - feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" "github.com/tech/sendico/pkg/server/grpcapp" "go.uber.org/zap" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" "gopkg.in/yaml.v3" ) @@ -26,29 +22,13 @@ type Imp struct { file string debug bool - config *config - app *grpcapp.App[storage.Repository] - service *ledger.Service - feesConn *grpc.ClientConn + config *config + app *grpcapp.App[storage.Repository] + service *ledger.Service } type config struct { *grpcapp.Config `yaml:",inline"` - Fees FeesClientConfig `yaml:"fees"` -} - -type FeesClientConfig struct { - Address string `yaml:"address"` - TimeoutSeconds int `yaml:"timeout_seconds"` -} - -const defaultFeesTimeout = 3 * time.Second - -func (c FeesClientConfig) timeout() time.Duration { - if c.TimeoutSeconds <= 0 { - return defaultFeesTimeout - } - return time.Duration(c.TimeoutSeconds) * time.Second } func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { @@ -64,9 +44,6 @@ func (i *Imp) Shutdown() { if i.service != nil { i.service.Shutdown() } - if i.feesConn != nil { - _ = i.feesConn.Close() - } return } @@ -82,10 +59,6 @@ func (i *Imp) Shutdown() { ctx, cancel := context.WithTimeout(context.Background(), timeout) i.app.Shutdown(ctx) cancel() - - if i.feesConn != nil { - _ = i.feesConn.Close() - } } func (i *Imp) Start() error { @@ -99,22 +72,6 @@ func (i *Imp) Start() error { return mongostorage.New(logger, conn) } - var feesClient feesv1.FeeEngineClient - feesTimeout := cfg.Fees.timeout() - if addr := strings.TrimSpace(cfg.Fees.Address); addr != "" { - ctx, cancel := context.WithTimeout(context.Background(), feesTimeout) - defer cancel() - - conn, err := grpc.DialContext(ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - i.logger.Warn("Failed to connect to fees service", zap.String("address", addr), zap.Error(err)) - } else { - i.logger.Info("Connected to fees service", zap.String("address", addr)) - i.feesConn = conn - feesClient = feesv1.NewFeeEngineClient(conn) - } - } - serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { invokeURI := "" if cfg.GRPC != nil { @@ -124,7 +81,7 @@ func (i *Imp) Start() error { if cfg.Messaging != nil { msgSettings = cfg.Messaging.Settings } - svc, err := ledger.NewService(logger, repo, producer, msgSettings, feesClient, feesTimeout, invokeURI) + svc, err := ledger.NewService(logger, repo, producer, msgSettings, invokeURI) if err != nil { return nil, err } diff --git a/api/ledger/internal/service/ledger/posting.go b/api/ledger/internal/service/ledger/posting.go index 09180631..c8e70a33 100644 --- a/api/ledger/internal/service/ledger/posting.go +++ b/api/ledger/internal/service/ledger/posting.go @@ -97,13 +97,6 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi entryTotal := creditAmount charges := req.Charges - if len(charges) == 0 { - if computed, err := s.quoteFeesForCredit(ctx, req); err != nil { - logger.Warn("Failed to quote fees", zap.Error(err)) - } else if len(computed) > 0 { - charges = computed - } - } if err := validatePostingLines(charges); err != nil { return nil, err } diff --git a/api/ledger/internal/service/ledger/posting_debit.go b/api/ledger/internal/service/ledger/posting_debit.go index 41e69d31..4534c671 100644 --- a/api/ledger/internal/service/ledger/posting_debit.go +++ b/api/ledger/internal/service/ledger/posting_debit.go @@ -94,13 +94,6 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR entryTotal := debitAmount.Neg() charges := req.Charges - if len(charges) == 0 { - if computed, err := s.quoteFeesForDebit(ctx, req); err != nil { - logger.Warn("Failed to quote fees", zap.Error(err)) - } else if len(computed) > 0 { - charges = computed - } - } if err := validatePostingLines(charges); err != nil { return nil, err } diff --git a/api/ledger/internal/service/ledger/posting_external.go b/api/ledger/internal/service/ledger/posting_external.go index ee1beedb..5dc6a133 100644 --- a/api/ledger/internal/service/ledger/posting_external.go +++ b/api/ledger/internal/service/ledger/posting_external.go @@ -111,13 +111,6 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P entryTotal := creditAmount charges := req.Charges - if len(charges) == 0 { - if computed, err := s.quoteFeesForCredit(ctx, req); err != nil { - logger.Warn("Failed to quote fees", zap.Error(err)) - } else if len(computed) > 0 { - charges = computed - } - } if err := validatePostingLines(charges); err != nil { return nil, err } @@ -344,13 +337,6 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po entryTotal := debitAmount.Neg() charges := req.Charges - if len(charges) == 0 { - if computed, err := s.quoteFeesForDebit(ctx, req); err != nil { - logger.Warn("Failed to quote fees", zap.Error(err)) - } else if len(computed) > 0 { - charges = computed - } - } if err := validatePostingLines(charges); err != nil { return nil, err } diff --git a/api/ledger/internal/service/ledger/service.go b/api/ledger/internal/service/ledger/service.go index 29b6ca3c..86fc8e36 100644 --- a/api/ledger/internal/service/ledger/service.go +++ b/api/ledger/internal/service/ledger/service.go @@ -2,26 +2,17 @@ package ledger import ( "context" - "fmt" "strings" "sync" "time" - "github.com/shopspring/decimal" - feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" "go.uber.org/zap" "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/timestamppb" - - accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1" - moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" - tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" "github.com/tech/sendico/ledger/internal/appversion" "github.com/tech/sendico/ledger/storage" "github.com/tech/sendico/pkg/api/routers" "github.com/tech/sendico/pkg/discovery" - "github.com/tech/sendico/pkg/merrors" pmessaging "github.com/tech/sendico/pkg/messaging" pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable" "github.com/tech/sendico/pkg/mlogger" @@ -46,7 +37,6 @@ type Service struct { storage storage.Repository producer pmessaging.Producer msgCfg pmodel.SettingsT - fees feesDependency announcer *discovery.Announcer invokeURI string @@ -63,16 +53,7 @@ type Service struct { } } -type feesDependency struct { - client feesv1.FeeEngineClient - timeout time.Duration -} - -func (f feesDependency) available() bool { - return f.client != nil -} - -func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer, msgCfg pmodel.SettingsT, feesClient feesv1.FeeEngineClient, feesTimeout time.Duration, invokeURI string) (*Service, error) { +func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer, msgCfg pmodel.SettingsT, invokeURI string) (*Service, error) { // Initialize Prometheus metrics initMetrics() @@ -82,10 +63,6 @@ func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging. producer: prod, msgCfg: msgCfg, invokeURI: strings.TrimSpace(invokeURI), - fees: feesDependency{ - client: feesClient, - timeout: feesTimeout, - }, } if err := service.startOutboxReliableProducer(); err != nil { @@ -493,190 +470,3 @@ func (s *Service) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRe responder := s.getStatementResponder(ctx, req) return responder(ctx) } - -func (s *Service) quoteFeesForCredit(ctx context.Context, req *ledgerv1.PostCreditRequest) ([]*ledgerv1.PostingLine, error) { - if !s.fees.available() { - return nil, nil - } - attrs := map[string]string{} - if strings.TrimSpace(req.GetDescription()) != "" { - attrs["description"] = req.GetDescription() - } - return s.quoteFees(ctx, feesv1.Trigger_TRIGGER_CAPTURE, req.GetOrganizationRef(), req.GetIdempotencyKey(), req.GetLedgerAccountRef(), "ledger.post_credit", req.GetIdempotencyKey(), req.GetEventTime(), req.Money, attrs) -} - -func (s *Service) quoteFeesForDebit(ctx context.Context, req *ledgerv1.PostDebitRequest) ([]*ledgerv1.PostingLine, error) { - if !s.fees.available() { - return nil, nil - } - attrs := map[string]string{} - if strings.TrimSpace(req.GetDescription()) != "" { - attrs["description"] = req.GetDescription() - } - return s.quoteFees(ctx, feesv1.Trigger_TRIGGER_REFUND, req.GetOrganizationRef(), req.GetIdempotencyKey(), req.GetLedgerAccountRef(), "ledger.post_debit", req.GetIdempotencyKey(), req.GetEventTime(), req.Money, attrs) -} - -func (s *Service) quoteFees(ctx context.Context, trigger feesv1.Trigger, organizationRef, idempotencyKey, ledgerAccountRef, originType, originRef string, eventTime *timestamppb.Timestamp, baseAmount *moneyv1.Money, attributes map[string]string) ([]*ledgerv1.PostingLine, error) { - if !s.fees.available() { - return nil, nil - } - if strings.TrimSpace(organizationRef) == "" { - return nil, merrors.InvalidArgument("organization reference is required to quote fees") - } - if baseAmount == nil { - return nil, merrors.InvalidArgument("base amount is required to quote fees") - } - - amountCopy := &moneyv1.Money{Amount: baseAmount.GetAmount(), Currency: baseAmount.GetCurrency()} - bookedAt := eventTime - if bookedAt == nil { - bookedAt = timestamppb.Now() - } - - trace := &tracev1.TraceContext{ - RequestRef: idempotencyKey, - IdempotencyKey: idempotencyKey, - } - - req := &feesv1.QuoteFeesRequest{ - Meta: &feesv1.RequestMeta{ - OrganizationRef: organizationRef, - Trace: trace, - }, - Intent: &feesv1.Intent{ - Trigger: trigger, - BaseAmount: amountCopy, - BookedAt: bookedAt, - OriginType: originType, - OriginRef: originRef, - Attributes: map[string]string{}, - }, - } - - setFeeAttributeIfMissing(req.Intent.Attributes, "product", "ledger") - setFeeAttributeIfMissing(req.Intent.Attributes, "operation", ledgerOperation(originType, trigger)) - setFeeAttributeIfMissing(req.Intent.Attributes, "currency", strings.TrimSpace(baseAmount.GetCurrency())) - - if ledgerAccountRef != "" { - req.Intent.Attributes["ledger_account_ref"] = ledgerAccountRef - } - for k, v := range attributes { - if strings.TrimSpace(k) == "" { - continue - } - req.Intent.Attributes[k] = v - } - - callCtx := ctx - if s.fees.timeout > 0 { - var cancel context.CancelFunc - callCtx, cancel = context.WithTimeout(ctx, s.fees.timeout) - defer cancel() - } - - resp, err := s.fees.client.QuoteFees(callCtx, req) - if err != nil { - return nil, err - } - - lines, err := convertFeeDerivedLines(resp.GetLines()) - if err != nil { - return nil, err - } - return lines, nil -} - -func convertFeeDerivedLines(lines []*feesv1.DerivedPostingLine) ([]*ledgerv1.PostingLine, error) { - result := make([]*ledgerv1.PostingLine, 0, len(lines)) - for idx, line := range lines { - if line == nil { - continue - } - if line.GetMoney() == nil { - return nil, merrors.Internal(fmt.Sprintf("fee line %d missing money", idx)) - } - dec, err := decimal.NewFromString(line.GetMoney().GetAmount()) - if err != nil { - return nil, merrors.InternalWrap(err, fmt.Sprintf("fee line %d invalid amount", idx)) - } - dec = ensureAmountForSide(dec, line.GetSide()) - posting := &ledgerv1.PostingLine{ - LedgerAccountRef: line.GetLedgerAccountRef(), - Money: &moneyv1.Money{ - Amount: dec.String(), - Currency: line.GetMoney().GetCurrency(), - }, - LineType: mapFeeLineType(line.GetLineType()), - } - result = append(result, posting) - } - return result, nil -} - -func ensureAmountForSide(amount decimal.Decimal, side accountingv1.EntrySide) decimal.Decimal { - switch side { - case accountingv1.EntrySide_ENTRY_SIDE_DEBIT: - if amount.Sign() > 0 { - return amount.Neg() - } - case accountingv1.EntrySide_ENTRY_SIDE_CREDIT: - if amount.Sign() < 0 { - return amount.Neg() - } - } - return amount -} - -func setFeeAttributeIfMissing(attrs map[string]string, key, value string) { - if attrs == nil { - return - } - if strings.TrimSpace(key) == "" { - return - } - value = strings.TrimSpace(value) - if value == "" { - return - } - if _, exists := attrs[key]; exists { - return - } - attrs[key] = value -} - -func ledgerOperation(originType string, trigger feesv1.Trigger) string { - originType = strings.TrimSpace(originType) - if originType != "" { - parts := strings.SplitN(originType, ".", 2) - if len(parts) == 2 && strings.TrimSpace(parts[1]) != "" { - return strings.TrimSpace(parts[1]) - } - } - switch trigger { - case feesv1.Trigger_TRIGGER_CAPTURE: - return "credit" - case feesv1.Trigger_TRIGGER_REFUND: - return "debit" - case feesv1.Trigger_TRIGGER_PAYOUT: - return "payout" - case feesv1.Trigger_TRIGGER_DISPUTE: - return "dispute" - case feesv1.Trigger_TRIGGER_FX_CONVERSION: - return "fx_conversion" - default: - return "" - } -} - -func mapFeeLineType(lineType accountingv1.PostingLineType) ledgerv1.LineType { - switch lineType { - case accountingv1.PostingLineType_POSTING_LINE_FEE: - return ledgerv1.LineType_LINE_FEE - case accountingv1.PostingLineType_POSTING_LINE_SPREAD: - return ledgerv1.LineType_LINE_SPREAD - case accountingv1.PostingLineType_POSTING_LINE_REVERSAL: - return ledgerv1.LineType_LINE_REVERSAL - default: - return ledgerv1.LineType_LINE_FEE - } -} diff --git a/api/pkg/chain/asset.go b/api/pkg/chain/asset.go index 22addbe0..72ec287b 100644 --- a/api/pkg/chain/asset.go +++ b/api/pkg/chain/asset.go @@ -43,7 +43,7 @@ func ParseAsset(assetString, network, tokenSymbol, contractAddress string) (*cha return &chainv1.Asset{ Chain: chain, TokenSymbol: strings.ToUpper(token), - ContractAddress: strings.ToLower(contract), + ContractAddress: contract, }, nil } diff --git a/api/pkg/chain/asset_test.go b/api/pkg/chain/asset_test.go index cd38530c..c502d808 100644 --- a/api/pkg/chain/asset_test.go +++ b/api/pkg/chain/asset_test.go @@ -47,6 +47,11 @@ func TestParseAsset(t *testing.T) { require.Equal(t, "USDT", asset.GetTokenSymbol()) require.Equal(t, "", asset.GetContractAddress()) + contract := "TR7NhQjeKQxGTCi8q8ZY4pL8otSzgjLj6T" + asset, err = ParseAsset("USDT-TRC20", "TRON_MAINNET", "", contract) + require.NoError(t, err) + require.Equal(t, contract, asset.GetContractAddress()) + asset, err = ParseAsset("USDT-TRC20", "CHAIN_NETWORK_TRON_MAINNET", "", "") require.NoError(t, err) require.Equal(t, chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET, asset.GetChain()) diff --git a/api/server/go.sum b/api/server/go.sum index 7efa7df5..d17cbdc0 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -364,8 +364,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc h1:ULD+ToGXUIU6Pkzr1ARxdyvwfHbelw+agoFDRbLg4TU= -google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= diff --git a/api/server/internal/server/accountapiimp/service.go b/api/server/internal/server/accountapiimp/service.go index 53503589..cf42bc50 100644 --- a/api/server/internal/server/accountapiimp/service.go +++ b/api/server/internal/server/accountapiimp/service.go @@ -7,8 +7,8 @@ import ( "strings" "time" - ledgerclient "github.com/tech/sendico/ledger/client" trongatewayclient "github.com/tech/sendico/gateway/tron/client" + ledgerclient "github.com/tech/sendico/ledger/client" api "github.com/tech/sendico/pkg/api/http" "github.com/tech/sendico/pkg/auth" "github.com/tech/sendico/pkg/db/account" @@ -256,7 +256,7 @@ func buildGatewayAsset(cfg eapi.ChainGatewayAssetConfig) (*chainv1.Asset, error) return &chainv1.Asset{ Chain: chain, TokenSymbol: strings.ToUpper(tokenSymbol), - ContractAddress: strings.ToLower(strings.TrimSpace(cfg.ContractAddress)), + ContractAddress: strings.TrimSpace(cfg.ContractAddress), }, nil } diff --git a/api/server/internal/server/accountapiimp/service_test.go b/api/server/internal/server/accountapiimp/service_test.go new file mode 100644 index 00000000..4710d697 --- /dev/null +++ b/api/server/internal/server/accountapiimp/service_test.go @@ -0,0 +1,19 @@ +package accountapiimp + +import ( + "testing" + + "github.com/stretchr/testify/require" + eapi "github.com/tech/sendico/server/interface/api" +) + +func TestBuildGatewayAsset_PreservesContractAddressCase(t *testing.T) { + asset, err := buildGatewayAsset(eapi.ChainGatewayAssetConfig{ + Chain: "TRON_MAINNET", + TokenSymbol: "usdt", + ContractAddress: "TR7NhQjeKQxGTCi8q8ZY4pL8otSzgjLj6T", + }) + require.NoError(t, err) + require.Equal(t, "USDT", asset.GetTokenSymbol()) + require.Equal(t, "TR7NhQjeKQxGTCi8q8ZY4pL8otSzgjLj6T", asset.GetContractAddress()) +}