Compare commits
5 Commits
a6374d1136
...
SEND018
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad3d44f137 | ||
|
|
f339630115 | ||
|
|
75d5a512cd | ||
|
|
1811571f80 | ||
|
|
edfdef5211 |
@@ -130,7 +130,7 @@ func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteR
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, merrors.ErrNoData):
|
||||
logger.Warn("Pair not supported", zap.String("pair", pairKey.Base+"/"+pairKey.Quote))
|
||||
logger.Warn("pair not supported", zap.String("pair", pairKey.Base+"/"+pairKey.Quote))
|
||||
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, merrors.InvalidArgument("pair_not_supported"))
|
||||
default:
|
||||
logger.Warn("GetQuote failed to load pair", zap.Error(err))
|
||||
@@ -150,7 +150,7 @@ func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteR
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, merrors.ErrNoData):
|
||||
logger.Warn("Rate not found", zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", provider))
|
||||
logger.Warn("rate not found", zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", provider))
|
||||
return gsresponse.FailedPrecondition[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, "rate_not_found", err)
|
||||
default:
|
||||
logger.Warn("GetQuote failed to load rate", zap.Error(err), zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", provider))
|
||||
|
||||
@@ -50,28 +50,28 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
|
||||
defer cancel()
|
||||
|
||||
if err := s.Ping(ctx); err != nil {
|
||||
s.logger.Error("Mongo ping failed during store init", zap.Error(err))
|
||||
s.logger.Error("mongo ping failed during store init", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ratesStore, err := store.NewRates(s.logger, db)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to initialize rates store", zap.Error(err))
|
||||
s.logger.Error("failed to initialize rates store", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
quotesStore, err := store.NewQuotes(s.logger, db, txFactory)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to initialize quotes store", zap.Error(err))
|
||||
s.logger.Error("failed to initialize quotes store", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
pairsStore, err := store.NewPair(s.logger, db)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to initialize pair store", zap.Error(err))
|
||||
s.logger.Error("failed to initialize pair store", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
currencyStore, err := store.NewCurrency(s.logger, db)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to initialize currency store", zap.Error(err))
|
||||
s.logger.Error("failed to initialize currency store", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
|
||||
s.pairs = pairsStore
|
||||
s.currencies = currencyStore
|
||||
|
||||
s.logger.Info("Mongo storage ready")
|
||||
s.logger.Info("mongo storage ready")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -29,11 +29,11 @@ func NewCurrency(logger mlogger.Logger, db *mongo.Database) (storage.CurrencySto
|
||||
Unique: true,
|
||||
}
|
||||
if err := repo.CreateIndex(index); err != nil {
|
||||
logger.Error("Failed to ensure currencies index", zap.Error(err))
|
||||
logger.Error("failed to ensure currencies index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
childLogger := logger.Named(model.CurrenciesCollection)
|
||||
childLogger.Debug("Currency store initialised", zap.String("collection", model.CurrenciesCollection))
|
||||
childLogger.Debug("currency store initialised", zap.String("collection", model.CurrenciesCollection))
|
||||
|
||||
return ¤cyStore{
|
||||
logger: childLogger,
|
||||
@@ -43,17 +43,17 @@ func NewCurrency(logger mlogger.Logger, db *mongo.Database) (storage.CurrencySto
|
||||
|
||||
func (c *currencyStore) Get(ctx context.Context, code string) (*model.Currency, error) {
|
||||
if code == "" {
|
||||
c.logger.Warn("Attempt to fetch currency with empty code")
|
||||
c.logger.Warn("attempt to fetch currency with empty code")
|
||||
return nil, merrors.InvalidArgument("currencyStore: empty code")
|
||||
}
|
||||
result := &model.Currency{}
|
||||
if err := c.repo.FindOneByFilter(ctx, repository.Filter("code", code), result); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
c.logger.Debug("Currency not found", zap.String("code", code))
|
||||
c.logger.Debug("currency not found", zap.String("code", code))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
c.logger.Debug("Currency loaded", zap.String("code", code))
|
||||
c.logger.Debug("currency loaded", zap.String("code", code))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -77,20 +77,20 @@ func (c *currencyStore) List(ctx context.Context, codes ...string) ([]*model.Cur
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to list currencies", zap.Error(err))
|
||||
c.logger.Error("failed to list currencies", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
c.logger.Debug("Listed currencies", zap.Int("count", len(currencies)))
|
||||
c.logger.Debug("listed currencies", zap.Int("count", len(currencies)))
|
||||
return currencies, nil
|
||||
}
|
||||
|
||||
func (c *currencyStore) Upsert(ctx context.Context, currency *model.Currency) error {
|
||||
if currency == nil {
|
||||
c.logger.Warn("Attempt to upsert nil currency")
|
||||
c.logger.Warn("attempt to upsert nil currency")
|
||||
return merrors.InvalidArgument("currencyStore: nil currency")
|
||||
}
|
||||
if currency.Code == "" {
|
||||
c.logger.Warn("Attempt to upsert currency with empty code")
|
||||
c.logger.Warn("attempt to upsert currency with empty code")
|
||||
return merrors.InvalidArgument("currencyStore: empty code")
|
||||
}
|
||||
|
||||
@@ -98,16 +98,16 @@ func (c *currencyStore) Upsert(ctx context.Context, currency *model.Currency) er
|
||||
filter := repository.Filter("code", currency.Code)
|
||||
if err := c.repo.FindOneByFilter(ctx, filter, existing); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
c.logger.Debug("Inserting new currency", zap.String("code", currency.Code))
|
||||
c.logger.Debug("inserting new currency", zap.String("code", currency.Code))
|
||||
return c.repo.Insert(ctx, currency, filter)
|
||||
}
|
||||
c.logger.Warn("Failed to fetch currency", zap.Error(err), zap.String("code", currency.Code))
|
||||
c.logger.Error("failed to fetch currency", zap.Error(err), zap.String("code", currency.Code))
|
||||
return err
|
||||
}
|
||||
|
||||
if existing.GetID() != nil {
|
||||
currency.SetID(*existing.GetID())
|
||||
}
|
||||
c.logger.Debug("Updating currency", zap.String("code", currency.Code))
|
||||
c.logger.Debug("updating currency", zap.String("code", currency.Code))
|
||||
return c.repo.Update(ctx, currency)
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@ func NewPair(logger mlogger.Logger, db *mongo.Database) (storage.PairStore, erro
|
||||
Unique: true,
|
||||
}
|
||||
if err := repo.CreateIndex(index); err != nil {
|
||||
logger.Error("Failed to ensure pairs index", zap.Error(err))
|
||||
logger.Error("failed to ensure pairs index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
logger.Debug("Pair store initialised", zap.String("collection", model.PairsCollection))
|
||||
logger.Debug("pair store initialised", zap.String("collection", model.PairsCollection))
|
||||
|
||||
return &pairStore{
|
||||
logger: logger.Named(model.PairsCollection),
|
||||
@@ -53,16 +53,16 @@ func (p *pairStore) ListEnabled(ctx context.Context) ([]*model.Pair, error) {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
p.logger.Warn("Failed to list enabled pairs", zap.Error(err))
|
||||
p.logger.Error("failed to list enabled pairs", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Debug("Listed enabled pairs", zap.Int("count", len(pairs)))
|
||||
p.logger.Debug("listed enabled pairs", zap.Int("count", len(pairs)))
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
func (p *pairStore) Get(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
|
||||
if pair.Base == "" || pair.Quote == "" {
|
||||
p.logger.Warn("Attempt to fetch pair with empty currency", zap.String("base", pair.Base), zap.String("quote", pair.Quote))
|
||||
p.logger.Warn("attempt to fetch pair with empty currency", zap.String("base", pair.Base), zap.String("quote", pair.Quote))
|
||||
return nil, merrors.InvalidArgument("pairStore: incomplete pair")
|
||||
}
|
||||
result := &model.Pair{}
|
||||
@@ -71,21 +71,21 @@ func (p *pairStore) Get(ctx context.Context, pair model.CurrencyPair) (*model.Pa
|
||||
Filter(repository.Field("pair").Dot("quote"), pair.Quote)
|
||||
if err := p.repo.FindOneByFilter(ctx, query, result); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
p.logger.Debug("Pair not found", zap.String("base", pair.Base), zap.String("quote", pair.Quote))
|
||||
p.logger.Debug("pair not found", zap.String("base", pair.Base), zap.String("quote", pair.Quote))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Debug("Pair loaded", zap.String("base", pair.Base), zap.String("quote", pair.Quote))
|
||||
p.logger.Debug("pair loaded", zap.String("base", pair.Base), zap.String("quote", pair.Quote))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *pairStore) Upsert(ctx context.Context, pair *model.Pair) error {
|
||||
if pair == nil {
|
||||
p.logger.Warn("Attempt to upsert nil pair")
|
||||
p.logger.Warn("attempt to upsert nil pair")
|
||||
return merrors.InvalidArgument("pairStore: nil pair")
|
||||
}
|
||||
if pair.Pair.Base == "" || pair.Pair.Quote == "" {
|
||||
p.logger.Warn("Attempt to upsert pair with empty currency", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
p.logger.Warn("attempt to upsert pair with empty currency", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
return merrors.InvalidArgument("pairStore: incomplete pair")
|
||||
}
|
||||
|
||||
@@ -96,16 +96,16 @@ func (p *pairStore) Upsert(ctx context.Context, pair *model.Pair) error {
|
||||
err := p.repo.FindOneByFilter(ctx, query, existing)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
p.logger.Debug("Inserting new pair", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
p.logger.Debug("inserting new pair", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
return p.repo.Insert(ctx, pair, query)
|
||||
}
|
||||
p.logger.Warn("Failed to fetch pair", zap.Error(err), zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
p.logger.Error("failed to fetch pair", zap.Error(err), zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
return err
|
||||
}
|
||||
|
||||
if existing.GetID() != nil {
|
||||
pair.SetID(*existing.GetID())
|
||||
}
|
||||
p.logger.Debug("Updating pair", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
p.logger.Debug("updating pair", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
return p.repo.Update(ctx, pair)
|
||||
}
|
||||
|
||||
@@ -56,12 +56,12 @@ func NewQuotes(logger mlogger.Logger, db *mongo.Database, txFactory transaction.
|
||||
|
||||
for _, def := range indexes {
|
||||
if err := repo.CreateIndex(def); err != nil {
|
||||
logger.Error("Failed to ensure quotes index", zap.Error(err))
|
||||
logger.Error("failed to ensure quotes index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
childLogger := logger.Named(model.QuotesCollection)
|
||||
childLogger.Debug("Quotes store initialised", zap.String("collection", model.QuotesCollection))
|
||||
childLogger.Debug("quotes store initialised", zap.String("collection", model.QuotesCollection))
|
||||
|
||||
return "esStore{
|
||||
logger: childLogger,
|
||||
@@ -72,11 +72,11 @@ func NewQuotes(logger mlogger.Logger, db *mongo.Database, txFactory transaction.
|
||||
|
||||
func (q *quotesStore) Issue(ctx context.Context, quote *model.Quote) error {
|
||||
if quote == nil {
|
||||
q.logger.Warn("Attempt to issue nil quote")
|
||||
q.logger.Warn("attempt to issue nil quote")
|
||||
return merrors.InvalidArgument("quotesStore: nil quote")
|
||||
}
|
||||
if quote.QuoteRef == "" {
|
||||
q.logger.Warn("Attempt to issue quote with empty ref")
|
||||
q.logger.Warn("attempt to issue quote with empty ref")
|
||||
return merrors.InvalidArgument("quotesStore: empty quoteRef")
|
||||
}
|
||||
|
||||
@@ -89,32 +89,32 @@ func (q *quotesStore) Issue(ctx context.Context, quote *model.Quote) error {
|
||||
quote.ConsumedByLedgerTxnRef = ""
|
||||
quote.ConsumedAtUnixMs = nil
|
||||
if err := q.repo.Insert(ctx, quote, repository.Filter("quoteRef", quote.QuoteRef)); err != nil {
|
||||
q.logger.Warn("Failed to insert quote", zap.Error(err), zap.String("quote_ref", quote.QuoteRef))
|
||||
q.logger.Error("failed to insert quote", zap.Error(err), zap.String("quote_ref", quote.QuoteRef))
|
||||
return err
|
||||
}
|
||||
q.logger.Debug("Quote issued", zap.String("quote_ref", quote.QuoteRef), zap.Bool("firm", quote.Firm))
|
||||
q.logger.Debug("quote issued", zap.String("quote_ref", quote.QuoteRef), zap.Bool("firm", quote.Firm))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *quotesStore) GetByRef(ctx context.Context, quoteRef string) (*model.Quote, error) {
|
||||
if quoteRef == "" {
|
||||
q.logger.Warn("Attempt to fetch quote with empty ref")
|
||||
q.logger.Warn("attempt to fetch quote with empty ref")
|
||||
return nil, merrors.InvalidArgument("quotesStore: empty quoteRef")
|
||||
}
|
||||
quote := &model.Quote{}
|
||||
if err := q.repo.FindOneByFilter(ctx, repository.Filter("quoteRef", quoteRef), quote); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
q.logger.Debug("Quote not found", zap.String("quote_ref", quoteRef))
|
||||
q.logger.Debug("quote not found", zap.String("quote_ref", quoteRef))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
q.logger.Debug("Quote loaded", zap.String("quote_ref", quoteRef), zap.String("status", string(quote.Status)))
|
||||
q.logger.Debug("quote loaded", zap.String("quote_ref", quoteRef), zap.String("status", string(quote.Status)))
|
||||
return quote, nil
|
||||
}
|
||||
|
||||
func (q *quotesStore) Consume(ctx context.Context, quoteRef, ledgerTxnRef string, when time.Time) (*model.Quote, error) {
|
||||
if quoteRef == "" || ledgerTxnRef == "" {
|
||||
q.logger.Warn("Attempt to consume quote with missing identifiers", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
q.logger.Warn("attempt to consume quote with missing identifiers", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
return nil, merrors.InvalidArgument("quotesStore: missing identifiers")
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ func (q *quotesStore) Consume(ctx context.Context, quoteRef, ledgerTxnRef string
|
||||
when = time.Now()
|
||||
}
|
||||
|
||||
q.logger.Debug("Consuming quote", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
q.logger.Debug("consuming quote", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
txn := q.txFactory.CreateTransaction()
|
||||
result, err := txn.Execute(ctx, func(txCtx context.Context) (any, error) {
|
||||
quote := &model.Quote{}
|
||||
@@ -131,7 +131,7 @@ func (q *quotesStore) Consume(ctx context.Context, quoteRef, ledgerTxnRef string
|
||||
}
|
||||
|
||||
if !quote.Firm {
|
||||
q.logger.Warn("Quote not firm", zap.String("quote_ref", quoteRef))
|
||||
q.logger.Warn("quote not firm", zap.String("quote_ref", quoteRef))
|
||||
return nil, storage.ErrQuoteNotFirm
|
||||
}
|
||||
|
||||
@@ -140,16 +140,16 @@ func (q *quotesStore) Consume(ctx context.Context, quoteRef, ledgerTxnRef string
|
||||
if err := q.repo.Update(txCtx, quote); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q.logger.Info("Quote expired during consume", zap.String("quote_ref", quoteRef))
|
||||
q.logger.Info("quote expired during consume", zap.String("quote_ref", quoteRef))
|
||||
return nil, storage.ErrQuoteExpired
|
||||
}
|
||||
|
||||
if quote.Status == model.QuoteStatusConsumed {
|
||||
if quote.ConsumedByLedgerTxnRef == ledgerTxnRef {
|
||||
q.logger.Debug("Quote already consumed by ledger", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
q.logger.Debug("quote already consumed by ledger", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
return quote, nil
|
||||
}
|
||||
q.logger.Warn("Quote consumed by different ledger", zap.String("quote_ref", quoteRef), zap.String("existing_ledger_ref", quote.ConsumedByLedgerTxnRef))
|
||||
q.logger.Warn("quote consumed by different ledger", zap.String("quote_ref", quoteRef), zap.String("existing_ledger_ref", quote.ConsumedByLedgerTxnRef))
|
||||
return nil, storage.ErrQuoteConsumed
|
||||
}
|
||||
|
||||
@@ -157,11 +157,11 @@ func (q *quotesStore) Consume(ctx context.Context, quoteRef, ledgerTxnRef string
|
||||
if err := q.repo.Update(txCtx, quote); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q.logger.Info("Quote consumed", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
q.logger.Info("quote consumed", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
return quote, nil
|
||||
})
|
||||
if err != nil {
|
||||
q.logger.Warn("Quote consumption failed", zap.Error(err), zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
q.logger.Error("quote consumption failed", zap.Error(err), zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
return nil, err
|
||||
}
|
||||
quote, _ := result.(*model.Quote)
|
||||
@@ -173,7 +173,7 @@ func (q *quotesStore) Consume(ctx context.Context, quoteRef, ledgerTxnRef string
|
||||
|
||||
func (q *quotesStore) ExpireIssuedBefore(ctx context.Context, cutoff time.Time) (int, error) {
|
||||
if cutoff.IsZero() {
|
||||
q.logger.Warn("Attempt to expire quotes with zero cutoff")
|
||||
q.logger.Warn("attempt to expire quotes with zero cutoff")
|
||||
return 0, merrors.InvalidArgument("quotesStore: cutoff time is zero")
|
||||
}
|
||||
|
||||
@@ -188,11 +188,11 @@ func (q *quotesStore) ExpireIssuedBefore(ctx context.Context, cutoff time.Time)
|
||||
|
||||
updated, err := q.repo.PatchMany(ctx, filter, patch)
|
||||
if err != nil {
|
||||
q.logger.Warn("Failed to expire quotes", zap.Error(err))
|
||||
q.logger.Error("failed to expire quotes", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
if updated > 0 {
|
||||
q.logger.Info("Quotes expired", zap.Int("count", updated))
|
||||
q.logger.Info("quotes expired", zap.Int("count", updated))
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ func NewRates(logger mlogger.Logger, db *mongo.Database) (storage.RatesStore, er
|
||||
|
||||
for _, def := range indexes {
|
||||
if err := repo.CreateIndex(def); err != nil {
|
||||
logger.Error("Failed to ensure rates index", zap.Error(err))
|
||||
logger.Error("failed to ensure rates index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
logger.Debug("Rates store initialised", zap.String("collection", model.RatesCollection))
|
||||
logger.Debug("rates store initialised", zap.String("collection", model.RatesCollection))
|
||||
return &ratesStore{
|
||||
logger: logger.Named(model.RatesCollection),
|
||||
repo: repo,
|
||||
@@ -64,11 +64,11 @@ func NewRates(logger mlogger.Logger, db *mongo.Database) (storage.RatesStore, er
|
||||
|
||||
func (r *ratesStore) UpsertSnapshot(ctx context.Context, snapshot *model.RateSnapshot) error {
|
||||
if snapshot == nil {
|
||||
r.logger.Warn("Attempt to upsert nil snapshot")
|
||||
r.logger.Warn("attempt to upsert nil snapshot")
|
||||
return merrors.InvalidArgument("ratesStore: nil snapshot")
|
||||
}
|
||||
if snapshot.RateRef == "" {
|
||||
r.logger.Warn("Attempt to upsert snapshot with empty rate_ref")
|
||||
r.logger.Warn("attempt to upsert snapshot with empty rate_ref")
|
||||
return merrors.InvalidArgument("ratesStore: empty rateRef")
|
||||
}
|
||||
|
||||
@@ -82,17 +82,17 @@ func (r *ratesStore) UpsertSnapshot(ctx context.Context, snapshot *model.RateSna
|
||||
err := r.repo.FindOneByFilter(ctx, filter, existing)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
r.logger.Debug("Inserting new rate snapshot", zap.String("rate_ref", snapshot.RateRef))
|
||||
r.logger.Debug("inserting new rate snapshot", zap.String("rate_ref", snapshot.RateRef))
|
||||
return r.repo.Insert(ctx, snapshot, filter)
|
||||
}
|
||||
r.logger.Warn("Failed to query rate snapshot", zap.Error(err), zap.String("rate_ref", snapshot.RateRef))
|
||||
r.logger.Error("failed to query rate snapshot", zap.Error(err), zap.String("rate_ref", snapshot.RateRef))
|
||||
return err
|
||||
}
|
||||
|
||||
if existing.GetID() != nil {
|
||||
snapshot.SetID(*existing.GetID())
|
||||
}
|
||||
r.logger.Debug("Updating rate snapshot", zap.String("rate_ref", snapshot.RateRef))
|
||||
r.logger.Debug("updating rate snapshot", zap.String("rate_ref", snapshot.RateRef))
|
||||
return r.repo.Update(ctx, snapshot)
|
||||
}
|
||||
|
||||
|
||||
@@ -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-20251229120209-a0d175451f7b // indirect
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549 // 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
|
||||
|
||||
@@ -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-20251229120209-a0d175451f7b h1:g/wCbvJGhOAqfGBjWnqtD6CVsXdr3G4GCbjLR6z9kNw=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251229120209-a0d175451f7b/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549 h1:NERDcANvDCnspxdMEMLXOMnuITWIWrTQvvhEA8ewBBM=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549/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=
|
||||
|
||||
@@ -45,22 +45,22 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
|
||||
}
|
||||
address := strings.TrimSpace(cfg.Address)
|
||||
if address == "" {
|
||||
logger.Error("Vault address missing")
|
||||
logger.Error("vault address missing")
|
||||
return nil, merrors.InvalidArgument("vault key manager: address is required")
|
||||
}
|
||||
tokenEnv := strings.TrimSpace(cfg.TokenEnv)
|
||||
if tokenEnv == "" {
|
||||
logger.Error("Vault token env missing")
|
||||
logger.Error("vault token env missing")
|
||||
return nil, merrors.InvalidArgument("vault key manager: token_env is required")
|
||||
}
|
||||
token := strings.TrimSpace(os.Getenv(tokenEnv))
|
||||
if token == "" {
|
||||
logger.Error("Vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
|
||||
logger.Error("vault token missing; expected Vault Agent to export token", zap.String("env", tokenEnv))
|
||||
return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set (expected Vault Agent sink to populate it)")
|
||||
}
|
||||
mountPath := strings.Trim(strings.TrimSpace(cfg.MountPath), "/")
|
||||
if mountPath == "" {
|
||||
logger.Error("Vault mount path missing")
|
||||
logger.Error("vault mount path missing")
|
||||
return nil, merrors.InvalidArgument("vault key manager: mount_path is required")
|
||||
}
|
||||
keyPrefix := strings.Trim(strings.TrimSpace(cfg.KeyPrefix), "/")
|
||||
@@ -73,7 +73,7 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
|
||||
|
||||
client, err := api.NewClient(clientCfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create vault client", zap.Error(err))
|
||||
logger.Error("failed to create vault client", zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to create client: " + err.Error())
|
||||
}
|
||||
client.SetToken(token)
|
||||
@@ -94,17 +94,17 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
|
||||
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
|
||||
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
|
||||
if strings.TrimSpace(walletRef) == "" {
|
||||
m.logger.Warn("WalletRef missing for managed key creation", zap.String("network", network))
|
||||
m.logger.Warn("walletRef missing for managed key creation", zap.String("network", network))
|
||||
return nil, merrors.InvalidArgument("vault key manager: walletRef is required")
|
||||
}
|
||||
if strings.TrimSpace(network) == "" {
|
||||
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
||||
m.logger.Warn("network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
||||
return nil, merrors.InvalidArgument("vault key manager: network is required")
|
||||
}
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
||||
m.logger.Warn("failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to generate key: " + err.Error())
|
||||
}
|
||||
privateKeyBytes := crypto.FromECDSA(privateKey)
|
||||
@@ -115,7 +115,7 @@ func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string,
|
||||
|
||||
err = m.persistKey(ctx, walletRef, network, privateKeyBytes, publicKeyBytes, address)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
||||
m.logger.Warn("failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
||||
zeroBytes(privateKeyBytes)
|
||||
zeroBytes(publicKeyBytes)
|
||||
return nil, err
|
||||
@@ -123,7 +123,7 @@ func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string,
|
||||
zeroBytes(privateKeyBytes)
|
||||
zeroBytes(publicKeyBytes)
|
||||
|
||||
m.logger.Info("Managed wallet key created",
|
||||
m.logger.Info("managed wallet key created",
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.String("network", network),
|
||||
zap.String("address", strings.ToLower(address)),
|
||||
@@ -158,43 +158,43 @@ func (m *Manager) buildKeyID(network, walletRef string) string {
|
||||
// SignTransaction loads the key material from Vault and signs the transaction.
|
||||
func (m *Manager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
|
||||
if strings.TrimSpace(keyID) == "" {
|
||||
m.logger.Warn("Signing failed: empty key id")
|
||||
m.logger.Warn("signing failed: empty key id")
|
||||
return nil, merrors.InvalidArgument("vault key manager: keyID is required")
|
||||
}
|
||||
if tx == nil {
|
||||
m.logger.Warn("Signing failed: nil transaction", zap.String("key_id", keyID))
|
||||
m.logger.Warn("signing failed: nil transaction", zap.String("key_id", keyID))
|
||||
return nil, merrors.InvalidArgument("vault key manager: transaction is nil")
|
||||
}
|
||||
if chainID == nil {
|
||||
m.logger.Warn("Signing failed: nil chain id", zap.String("key_id", keyID))
|
||||
m.logger.Warn("signing failed: nil chain id", zap.String("key_id", keyID))
|
||||
return nil, merrors.InvalidArgument("vault key manager: chainID is nil")
|
||||
}
|
||||
|
||||
material, err := m.loadKey(ctx, keyID)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to load key material", zap.String("key_id", keyID), zap.Error(err))
|
||||
m.logger.Warn("failed to load key material", zap.String("key_id", keyID), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyBytes, err := hex.DecodeString(material.PrivateKey)
|
||||
if err != nil {
|
||||
m.logger.Warn("Invalid key material", zap.String("key_id", keyID), zap.Error(err))
|
||||
m.logger.Warn("invalid key material", zap.String("key_id", keyID), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: invalid key material: " + err.Error())
|
||||
}
|
||||
defer zeroBytes(keyBytes)
|
||||
|
||||
privateKey, err := crypto.ToECDSA(keyBytes)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to construct private key", zap.String("key_id", keyID), zap.Error(err))
|
||||
m.logger.Warn("failed to construct private key", zap.String("key_id", keyID), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to construct private key: " + err.Error())
|
||||
}
|
||||
|
||||
signed, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), privateKey)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
|
||||
m.logger.Warn("failed to sign transaction", zap.String("key_id", keyID), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to sign transaction: " + err.Error())
|
||||
}
|
||||
m.logger.Info("Transaction signed with managed key",
|
||||
m.logger.Info("transaction signed with managed key",
|
||||
zap.String("key_id", keyID),
|
||||
zap.String("network", material.Network),
|
||||
zap.String("tx_hash", signed.Hash().Hex()),
|
||||
@@ -213,23 +213,23 @@ func (m *Manager) loadKey(ctx context.Context, keyID string) (*keyMaterial, erro
|
||||
secretPath := strings.Trim(strings.TrimPrefix(keyID, "/"), "/")
|
||||
secret, err := m.store.Get(ctx, secretPath)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to read secret", zap.String("path", secretPath), zap.Error(err))
|
||||
m.logger.Warn("failed to read secret", zap.String("path", secretPath), zap.Error(err))
|
||||
return nil, merrors.Internal("vault key manager: failed to read secret at " + secretPath + ": " + err.Error())
|
||||
}
|
||||
if secret == nil || secret.Data == nil {
|
||||
m.logger.Warn("Secret not found", zap.String("path", secretPath))
|
||||
m.logger.Warn("secret not found", zap.String("path", secretPath))
|
||||
return nil, merrors.NoData("vault key manager: secret " + secretPath + " not found")
|
||||
}
|
||||
|
||||
getString := func(key string) (string, error) {
|
||||
val, ok := secret.Data[key]
|
||||
if !ok {
|
||||
m.logger.Warn("Secret missing field", zap.String("path", secretPath), zap.String("field", key))
|
||||
m.logger.Warn("secret missing field", zap.String("path", secretPath), zap.String("field", key))
|
||||
return "", merrors.Internal("vault key manager: secret " + secretPath + " missing " + key)
|
||||
}
|
||||
str, ok := val.(string)
|
||||
if !ok || strings.TrimSpace(str) == "" {
|
||||
m.logger.Warn("Secret field invalid", zap.String("path", secretPath), zap.String("field", key))
|
||||
m.logger.Warn("secret field invalid", zap.String("path", secretPath), zap.String("field", key))
|
||||
return "", merrors.Internal("vault key manager: secret " + secretPath + " invalid " + key)
|
||||
}
|
||||
return str, nil
|
||||
|
||||
@@ -123,12 +123,12 @@ func (i *Imp) Start() error {
|
||||
cl := i.logger.Named("config")
|
||||
networkConfigs, err := resolveNetworkConfigs(cl.Named("network"), cfg.Chains)
|
||||
if err != nil {
|
||||
i.logger.Error("Invalid chain network configuration", zap.Error(err))
|
||||
i.logger.Error("invalid chain network configuration", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
rpcClients, err := rpcclient.Prepare(context.Background(), i.logger.Named("rpc"), networkConfigs)
|
||||
if err != nil {
|
||||
i.logger.Error("Failed to prepare rpc clients", zap.Error(err))
|
||||
i.logger.Error("failed to prepare rpc clients", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
i.rpcClients = rpcClients
|
||||
@@ -166,7 +166,7 @@ func (i *Imp) Start() error {
|
||||
func (i *Imp) loadConfig() (*config, error) {
|
||||
data, err := os.ReadFile(i.file)
|
||||
if err != nil {
|
||||
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
|
||||
i.logger.Error("could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func (i *Imp) loadConfig() (*config, error) {
|
||||
Config: &grpcapp.Config{},
|
||||
}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
i.logger.Error("Failed to parse configuration", zap.Error(err))
|
||||
i.logger.Error("failed to parse configuration", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
||||
result := make([]gatewayshared.Network, 0, len(chains))
|
||||
for _, chain := range chains {
|
||||
if strings.TrimSpace(chain.Name) == "" {
|
||||
logger.Warn("Skipping unnamed chain configuration")
|
||||
logger.Warn("skipping unnamed chain configuration")
|
||||
continue
|
||||
}
|
||||
rpcURL := strings.TrimSpace(os.Getenv(chain.RPCURLEnv))
|
||||
@@ -210,7 +210,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
||||
for _, token := range chain.Tokens {
|
||||
symbol := strings.TrimSpace(token.Symbol)
|
||||
if symbol == "" {
|
||||
logger.Warn("Skipping token with empty symbol", zap.String("chain", chain.Name))
|
||||
logger.Warn("skipping token with empty symbol", zap.String("chain", chain.Name))
|
||||
continue
|
||||
}
|
||||
addr := strings.TrimSpace(token.Contract)
|
||||
@@ -220,9 +220,9 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
||||
}
|
||||
if addr == "" {
|
||||
if env != "" {
|
||||
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("env", env), zap.String("chain", chain.Name))
|
||||
logger.Warn("token contract not configured", zap.String("token", symbol), zap.String("env", env), zap.String("chain", chain.Name))
|
||||
} else {
|
||||
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("chain", chain.Name))
|
||||
logger.Warn("token contract not configured", zap.String("token", symbol), zap.String("chain", chain.Name))
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
||||
|
||||
gasPolicy, err := buildGasTopUpPolicy(chain.Name, chain.GasTopUpPolicy)
|
||||
if err != nil {
|
||||
logger.Error("Invalid gas top-up policy", zap.String("chain", chain.Name), zap.Error(err))
|
||||
logger.Error("invalid gas top-up policy", zap.String("chain", chain.Name), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -322,13 +322,13 @@ func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewa
|
||||
|
||||
if address == "" {
|
||||
if cfg.AddressEnv != "" {
|
||||
logger.Warn("Service wallet address not configured", zap.String("env", cfg.AddressEnv))
|
||||
logger.Warn("service wallet address not configured", zap.String("env", cfg.AddressEnv))
|
||||
} else {
|
||||
logger.Warn("Service wallet address not configured", zap.String("chain", cfg.Chain))
|
||||
logger.Warn("service wallet address not configured", zap.String("chain", cfg.Chain))
|
||||
}
|
||||
}
|
||||
if privateKey == "" {
|
||||
logger.Warn("Service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv))
|
||||
logger.Warn("service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv))
|
||||
}
|
||||
|
||||
return gatewayshared.ServiceWallet{
|
||||
@@ -342,7 +342,7 @@ func resolveKeyManager(logger mlogger.Logger, cfg keymanager.Config) (keymanager
|
||||
driver := strings.ToLower(strings.TrimSpace(string(cfg.Driver)))
|
||||
if driver == "" {
|
||||
err := merrors.InvalidArgument("key management driver is not configured")
|
||||
logger.Error("Key management driver missing")
|
||||
logger.Error("key management driver missing")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -351,19 +351,19 @@ func resolveKeyManager(logger mlogger.Logger, cfg keymanager.Config) (keymanager
|
||||
settings := vaultmanager.Config{}
|
||||
if len(cfg.Settings) > 0 {
|
||||
if err := mapstructure.Decode(cfg.Settings, &settings); err != nil {
|
||||
logger.Error("Failed to decode vault key manager settings", zap.Error(err), zap.Any("settings", cfg.Settings))
|
||||
logger.Error("failed to decode vault key manager settings", zap.Error(err), zap.Any("settings", cfg.Settings))
|
||||
return nil, merrors.InvalidArgument("invalid vault key manager settings: " + err.Error())
|
||||
}
|
||||
}
|
||||
manager, err := vaultmanager.New(logger, settings)
|
||||
if err != nil {
|
||||
logger.Error("Failed to initialise vault key manager", zap.Error(err))
|
||||
logger.Error("failed to initialise vault key manager", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return manager, nil
|
||||
default:
|
||||
err := merrors.InvalidArgument("unsupported key management driver: " + driver)
|
||||
logger.Error("Unsupported key management driver", zap.String("driver", driver))
|
||||
logger.Error("unsupported key management driver", zap.String("driver", driver))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,7 @@ func resolveDestination(ctx context.Context, deps Deps, dest *chainv1.TransferDe
|
||||
return model.TransferDestination{}, err
|
||||
}
|
||||
return model.TransferDestination{
|
||||
ExternalAddress: normalized,
|
||||
ExternalAddressOriginal: external,
|
||||
Memo: strings.TrimSpace(dest.GetMemo()),
|
||||
ExternalAddress: normalized,
|
||||
Memo: strings.TrimSpace(dest.GetMemo()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -34,12 +34,12 @@ func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.E
|
||||
|
||||
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
||||
if sourceWalletRef == "" {
|
||||
c.deps.Logger.Warn("Source wallet ref missing")
|
||||
c.deps.Logger.Warn("source wallet ref missing")
|
||||
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
||||
}
|
||||
amount := req.GetAmount()
|
||||
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||
c.deps.Logger.Warn("Amount missing or incomplete")
|
||||
c.deps.Logger.Warn("amount missing or incomplete")
|
||||
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
||||
}
|
||||
|
||||
|
||||
@@ -29,22 +29,22 @@ func NewComputeGasTopUp(deps Deps) *computeGasTopUpCommand {
|
||||
|
||||
func (c *computeGasTopUpCommand) Execute(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) gsresponse.Responder[chainv1.ComputeGasTopUpResponse] {
|
||||
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||
c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
|
||||
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||
return gsresponse.Unavailable[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
if req == nil {
|
||||
c.deps.Logger.Warn("Nil request")
|
||||
c.deps.Logger.Warn("nil request")
|
||||
return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("request is required"))
|
||||
}
|
||||
|
||||
walletRef := strings.TrimSpace(req.GetWalletRef())
|
||||
if walletRef == "" {
|
||||
c.deps.Logger.Warn("Wallet ref missing")
|
||||
c.deps.Logger.Warn("wallet ref missing")
|
||||
return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
||||
}
|
||||
estimatedFee := req.GetEstimatedTotalFee()
|
||||
if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" {
|
||||
c.deps.Logger.Warn("Estimated fee missing")
|
||||
c.deps.Logger.Warn("estimated fee missing")
|
||||
return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("estimated_total_fee is required"))
|
||||
}
|
||||
|
||||
@@ -71,37 +71,37 @@ func NewEnsureGasTopUp(deps Deps) *ensureGasTopUpCommand {
|
||||
|
||||
func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) gsresponse.Responder[chainv1.EnsureGasTopUpResponse] {
|
||||
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||
c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
|
||||
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||
return gsresponse.Unavailable[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
if req == nil {
|
||||
c.deps.Logger.Warn("Nil request")
|
||||
c.deps.Logger.Warn("nil request")
|
||||
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("request is required"))
|
||||
}
|
||||
|
||||
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
||||
if idempotencyKey == "" {
|
||||
c.deps.Logger.Warn("Idempotency key missing")
|
||||
c.deps.Logger.Warn("idempotency key missing")
|
||||
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
||||
}
|
||||
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
||||
if organizationRef == "" {
|
||||
c.deps.Logger.Warn("Organization ref missing")
|
||||
c.deps.Logger.Warn("organization ref missing")
|
||||
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
||||
}
|
||||
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
||||
if sourceWalletRef == "" {
|
||||
c.deps.Logger.Warn("Source wallet ref missing")
|
||||
c.deps.Logger.Warn("source wallet ref missing")
|
||||
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
||||
}
|
||||
targetWalletRef := strings.TrimSpace(req.GetTargetWalletRef())
|
||||
if targetWalletRef == "" {
|
||||
c.deps.Logger.Warn("Target wallet ref missing")
|
||||
c.deps.Logger.Warn("target wallet ref missing")
|
||||
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("target_wallet_ref is required"))
|
||||
}
|
||||
estimatedFee := req.GetEstimatedTotalFee()
|
||||
if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" {
|
||||
c.deps.Logger.Warn("Estimated fee missing")
|
||||
c.deps.Logger.Warn("estimated fee missing")
|
||||
return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("estimated_total_fee is required"))
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ func logDecision(logger mlogger.Logger, walletRef string, estimatedFee *moneyv1.
|
||||
zap.String("operation_type", decision.OperationType),
|
||||
)
|
||||
}
|
||||
logger.Info("Gas top-up decision", fields...)
|
||||
logger.Info("gas top-up decision", fields...)
|
||||
}
|
||||
|
||||
func amountString(m *moneyv1.Money) string {
|
||||
|
||||
@@ -22,25 +22,25 @@ func NewGetTransfer(deps Deps) *getTransferCommand {
|
||||
|
||||
func (c *getTransferCommand) Execute(ctx context.Context, req *chainv1.GetTransferRequest) gsresponse.Responder[chainv1.GetTransferResponse] {
|
||||
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||
c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
|
||||
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||
return gsresponse.Unavailable[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
if req == nil {
|
||||
c.deps.Logger.Warn("Nil request")
|
||||
c.deps.Logger.Warn("nil request")
|
||||
return gsresponse.InvalidArgument[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||
}
|
||||
transferRef := strings.TrimSpace(req.GetTransferRef())
|
||||
if transferRef == "" {
|
||||
c.deps.Logger.Warn("Transfer_ref missing")
|
||||
c.deps.Logger.Warn("transfer_ref missing")
|
||||
return gsresponse.InvalidArgument[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("transfer_ref is required"))
|
||||
}
|
||||
transfer, err := c.deps.Storage.Transfers().Get(ctx, transferRef)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
c.deps.Logger.Warn("Not found", zap.String("transfer_ref", transferRef))
|
||||
c.deps.Logger.Warn("not found", zap.String("transfer_ref", transferRef))
|
||||
return gsresponse.NotFound[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
c.deps.Logger.Warn("Storage get failed", zap.Error(err), zap.String("transfer_ref", transferRef))
|
||||
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("transfer_ref", transferRef))
|
||||
return gsresponse.Auto[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
return gsresponse.Success(&chainv1.GetTransferResponse{Transfer: toProtoTransfer(transfer)})
|
||||
|
||||
@@ -23,7 +23,7 @@ func NewListTransfers(deps Deps) *listTransfersCommand {
|
||||
|
||||
func (c *listTransfersCommand) Execute(ctx context.Context, req *chainv1.ListTransfersRequest) gsresponse.Responder[chainv1.ListTransfersResponse] {
|
||||
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||
c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
|
||||
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||
return gsresponse.Unavailable[chainv1.ListTransfersResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
filter := model.TransferFilter{}
|
||||
@@ -41,7 +41,7 @@ func (c *listTransfersCommand) Execute(ctx context.Context, req *chainv1.ListTra
|
||||
|
||||
result, err := c.deps.Storage.Transfers().List(ctx, filter)
|
||||
if err != nil {
|
||||
c.deps.Logger.Warn("Storage list failed", zap.Error(err))
|
||||
c.deps.Logger.Warn("storage list failed", zap.Error(err))
|
||||
return gsresponse.Auto[chainv1.ListTransfersResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
||||
return gsresponse.Unavailable[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
if req == nil {
|
||||
c.deps.Logger.Warn("Nil request")
|
||||
c.deps.Logger.Warn("nil request")
|
||||
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
||||
}
|
||||
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
||||
if organizationRef == "" {
|
||||
c.deps.Logger.Warn("Missing organization ref")
|
||||
c.deps.Logger.Warn("mMssing organization ref")
|
||||
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
||||
}
|
||||
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
||||
|
||||
@@ -29,41 +29,41 @@ func NewGetWalletBalance(deps Deps) *getWalletBalanceCommand {
|
||||
|
||||
func (c *getWalletBalanceCommand) Execute(ctx context.Context, req *chainv1.GetWalletBalanceRequest) gsresponse.Responder[chainv1.GetWalletBalanceResponse] {
|
||||
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||
c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
|
||||
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||
return gsresponse.Unavailable[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
if req == nil {
|
||||
c.deps.Logger.Warn("Nil request")
|
||||
c.deps.Logger.Warn("nil request")
|
||||
return gsresponse.InvalidArgument[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||
}
|
||||
walletRef := strings.TrimSpace(req.GetWalletRef())
|
||||
if walletRef == "" {
|
||||
c.deps.Logger.Warn("Wallet_ref missing")
|
||||
c.deps.Logger.Warn("wallet_ref missing")
|
||||
return gsresponse.InvalidArgument[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
||||
}
|
||||
wallet, err := c.deps.Storage.Wallets().Get(ctx, walletRef)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
c.deps.Logger.Warn("Not found", zap.String("wallet_ref", walletRef))
|
||||
c.deps.Logger.Warn("not found", zap.String("wallet_ref", walletRef))
|
||||
return gsresponse.NotFound[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
c.deps.Logger.Warn("Storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
|
||||
tokenBalance, nativeBalance, chainErr := OnChainWalletBalances(ctx, c.deps, wallet)
|
||||
if chainErr != nil {
|
||||
c.deps.Logger.Warn("On-chain balance fetch failed, attempting cached balance", zap.Error(chainErr), zap.String("wallet_ref", walletRef))
|
||||
c.deps.Logger.Warn("on-chain balance fetch failed, attempting cached balance", zap.Error(chainErr), zap.String("wallet_ref", walletRef))
|
||||
stored, err := c.deps.Storage.Wallets().GetBalance(ctx, walletRef)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
c.deps.Logger.Warn("Cached balance not found", zap.String("wallet_ref", walletRef))
|
||||
c.deps.Logger.Warn("cached balance not found", zap.String("wallet_ref", walletRef))
|
||||
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, chainErr)
|
||||
}
|
||||
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
if c.isCachedBalanceStale(stored) {
|
||||
c.deps.Logger.Info("Cached balance is stale",
|
||||
c.deps.Logger.Warn("cached balance is stale",
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.Time("calculated_at", stored.CalculatedAt),
|
||||
zap.Duration("ttl", c.cacheTTL()),
|
||||
@@ -116,7 +116,7 @@ func (c *getWalletBalanceCommand) persistCachedBalance(ctx context.Context, wall
|
||||
record.PendingInbound = zeroMoney(currency)
|
||||
record.PendingOutbound = zeroMoney(currency)
|
||||
if err := c.deps.Storage.Wallets().SaveBalance(ctx, record); err != nil {
|
||||
c.deps.Logger.Warn("Failed to cache wallet balance", zap.String("wallet_ref", walletRef), zap.Error(err))
|
||||
c.deps.Logger.Warn("failed to cache wallet balance", zap.String("wallet_ref", walletRef), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,59 +25,59 @@ func NewCreateManagedWallet(deps Deps) *createManagedWalletCommand {
|
||||
|
||||
func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.CreateManagedWalletRequest) gsresponse.Responder[chainv1.CreateManagedWalletResponse] {
|
||||
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||
c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
|
||||
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||
return gsresponse.Unavailable[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
if req == nil {
|
||||
c.deps.Logger.Warn("Nil request")
|
||||
c.deps.Logger.Warn("nil request")
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||
}
|
||||
|
||||
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
||||
if idempotencyKey == "" {
|
||||
c.deps.Logger.Warn("Missing idempotency key")
|
||||
c.deps.Logger.Warn("missing idempotency key")
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
||||
}
|
||||
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
||||
if organizationRef == "" {
|
||||
c.deps.Logger.Warn("Missing organization ref")
|
||||
c.deps.Logger.Warn("missing organization ref")
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
||||
}
|
||||
ownerRef := strings.TrimSpace(req.GetOwnerRef())
|
||||
if ownerRef == "" {
|
||||
c.deps.Logger.Warn("Missing owner ref")
|
||||
c.deps.Logger.Warn("missing owner ref")
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("owner_ref is required"))
|
||||
}
|
||||
|
||||
asset := req.GetAsset()
|
||||
if asset == nil {
|
||||
c.deps.Logger.Warn("Missing asset")
|
||||
c.deps.Logger.Warn("missing asset")
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
|
||||
}
|
||||
|
||||
chainKey, _ := shared.ChainKeyFromEnum(asset.GetChain())
|
||||
if chainKey == "" {
|
||||
c.deps.Logger.Warn("Unsupported chain", zap.Any("chain", asset.GetChain()))
|
||||
c.deps.Logger.Warn("unsupported chain", zap.Any("chain", asset.GetChain()))
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||
}
|
||||
networkCfg, ok := c.deps.Networks.Network(chainKey)
|
||||
if !ok {
|
||||
c.deps.Logger.Warn("Unsupported chain in config", zap.String("chain", chainKey))
|
||||
c.deps.Logger.Warn("unsupported chain in config", zap.String("chain", chainKey))
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||
}
|
||||
if c.deps.Drivers == nil {
|
||||
c.deps.Logger.Warn("Chain drivers missing", zap.String("chain", chainKey))
|
||||
c.deps.Logger.Warn("chain drivers missing", zap.String("chain", chainKey))
|
||||
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("chain drivers not configured"))
|
||||
}
|
||||
chainDriver, err := c.deps.Drivers.Driver(chainKey)
|
||||
if err != nil {
|
||||
c.deps.Logger.Warn("Unsupported chain driver", zap.String("chain", chainKey), zap.Error(err))
|
||||
c.deps.Logger.Warn("unsupported chain driver", zap.String("chain", chainKey), zap.Error(err))
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||
}
|
||||
|
||||
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
||||
if tokenSymbol == "" {
|
||||
c.deps.Logger.Warn("Missing token symbol")
|
||||
c.deps.Logger.Warn("missing token symbol")
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required"))
|
||||
}
|
||||
contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress()))
|
||||
@@ -85,7 +85,7 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
|
||||
if !strings.EqualFold(tokenSymbol, networkCfg.NativeToken) {
|
||||
contractAddress = shared.ResolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
|
||||
if contractAddress == "" {
|
||||
c.deps.Logger.Warn("Unsupported token", zap.String("token", tokenSymbol), zap.String("chain", chainKey))
|
||||
c.deps.Logger.Warn("unsupported token", zap.String("token", tokenSymbol), zap.String("chain", chainKey))
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
|
||||
}
|
||||
}
|
||||
@@ -93,22 +93,22 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
|
||||
|
||||
walletRef := shared.GenerateWalletRef()
|
||||
if c.deps.KeyManager == nil {
|
||||
c.deps.Logger.Warn("Key manager missing")
|
||||
c.deps.Logger.Warn("key manager missing")
|
||||
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager not configured"))
|
||||
}
|
||||
|
||||
keyInfo, err := c.deps.KeyManager.CreateManagedWalletKey(ctx, walletRef, chainKey)
|
||||
if err != nil {
|
||||
c.deps.Logger.Warn("Key manager error", zap.Error(err))
|
||||
c.deps.Logger.Warn("key manager error", zap.Error(err))
|
||||
return gsresponse.Auto[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
if keyInfo == nil || strings.TrimSpace(keyInfo.Address) == "" {
|
||||
c.deps.Logger.Warn("Key manager returned empty address")
|
||||
c.deps.Logger.Warn("key manager returned empty address")
|
||||
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
|
||||
}
|
||||
depositAddress, err := chainDriver.FormatAddress(keyInfo.Address)
|
||||
if err != nil {
|
||||
c.deps.Logger.Warn("Invalid derived deposit address", zap.String("wallet_ref", walletRef), zap.Error(err))
|
||||
c.deps.Logger.Warn("invalid derived deposit address", zap.String("wallet_ref", walletRef), zap.Error(err))
|
||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
|
||||
@@ -156,10 +156,10 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
|
||||
created, err := c.deps.Storage.Wallets().Create(ctx, wallet)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
c.deps.Logger.Debug("Wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey))
|
||||
c.deps.Logger.Debug("wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey))
|
||||
return gsresponse.Success(&chainv1.CreateManagedWalletResponse{Wallet: toProtoManagedWallet(created)})
|
||||
}
|
||||
c.deps.Logger.Warn("Storage create failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||
c.deps.Logger.Warn("storage create failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||
return gsresponse.Auto[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,25 +22,25 @@ func NewGetManagedWallet(deps Deps) *getManagedWalletCommand {
|
||||
|
||||
func (c *getManagedWalletCommand) Execute(ctx context.Context, req *chainv1.GetManagedWalletRequest) gsresponse.Responder[chainv1.GetManagedWalletResponse] {
|
||||
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||
c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
|
||||
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||
return gsresponse.Unavailable[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
if req == nil {
|
||||
c.deps.Logger.Warn("Nil request")
|
||||
c.deps.Logger.Warn("nil request")
|
||||
return gsresponse.InvalidArgument[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||
}
|
||||
walletRef := strings.TrimSpace(req.GetWalletRef())
|
||||
if walletRef == "" {
|
||||
c.deps.Logger.Warn("Wallet_ref missing")
|
||||
c.deps.Logger.Warn("wallet_ref missing")
|
||||
return gsresponse.InvalidArgument[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
||||
}
|
||||
wallet, err := c.deps.Storage.Wallets().Get(ctx, walletRef)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
c.deps.Logger.Warn("Not found", zap.String("wallet_ref", walletRef))
|
||||
c.deps.Logger.Warn("not found", zap.String("wallet_ref", walletRef))
|
||||
return gsresponse.NotFound[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
c.deps.Logger.Warn("Storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||
return gsresponse.Auto[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
return gsresponse.Success(&chainv1.GetManagedWalletResponse{Wallet: toProtoManagedWallet(wallet)})
|
||||
|
||||
@@ -23,7 +23,7 @@ func NewListManagedWallets(deps Deps) *listManagedWalletsCommand {
|
||||
|
||||
func (c *listManagedWalletsCommand) Execute(ctx context.Context, req *chainv1.ListManagedWalletsRequest) gsresponse.Responder[chainv1.ListManagedWalletsResponse] {
|
||||
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||
c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
|
||||
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||
return gsresponse.Unavailable[chainv1.ListManagedWalletsResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
filter := model.ManagedWalletFilter{}
|
||||
@@ -42,7 +42,7 @@ func (c *listManagedWalletsCommand) Execute(ctx context.Context, req *chainv1.Li
|
||||
|
||||
result, err := c.deps.Storage.Wallets().List(ctx, filter)
|
||||
if err != nil {
|
||||
c.deps.Logger.Warn("Storage list failed", zap.Error(err))
|
||||
c.deps.Logger.Warn("storage list failed", zap.Error(err))
|
||||
return gsresponse.Auto[chainv1.ListManagedWalletsResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,25 +27,25 @@ func (d *Driver) Name() string {
|
||||
}
|
||||
|
||||
func (d *Driver) FormatAddress(address string) (string, error) {
|
||||
d.logger.Debug("Format address", zap.String("address", address))
|
||||
d.logger.Debug("format address", zap.String("address", address))
|
||||
normalized, err := evm.NormalizeAddress(address)
|
||||
if err != nil {
|
||||
d.logger.Warn("Format address failed", zap.String("address", address), zap.Error(err))
|
||||
d.logger.Warn("format address failed", zap.String("address", address), zap.Error(err))
|
||||
}
|
||||
return normalized, err
|
||||
}
|
||||
|
||||
func (d *Driver) NormalizeAddress(address string) (string, error) {
|
||||
d.logger.Debug("Normalize address", zap.String("address", address))
|
||||
d.logger.Debug("normalize address", zap.String("address", address))
|
||||
normalized, err := evm.NormalizeAddress(address)
|
||||
if err != nil {
|
||||
d.logger.Warn("Normalize address failed", zap.String("address", address), zap.Error(err))
|
||||
d.logger.Warn("normalize address failed", zap.String("address", address), zap.Error(err))
|
||||
}
|
||||
return normalized, err
|
||||
}
|
||||
|
||||
func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||
d.logger.Debug("Balance request",
|
||||
d.logger.Debug("balance request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
)
|
||||
@@ -53,13 +53,13 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
||||
driverDeps.Logger = d.logger
|
||||
result, err := evm.Balance(ctx, driverDeps, network, wallet, wallet.DepositAddress)
|
||||
if err != nil {
|
||||
d.logger.Warn("Balance failed",
|
||||
d.logger.Warn("balance failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Balance result",
|
||||
d.logger.Debug("balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("amount", result.Amount),
|
||||
@@ -70,7 +70,7 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
||||
}
|
||||
|
||||
func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||
d.logger.Debug("Native balance request",
|
||||
d.logger.Debug("native balance request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
)
|
||||
@@ -78,13 +78,13 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
|
||||
driverDeps.Logger = d.logger
|
||||
result, err := evm.NativeBalance(ctx, driverDeps, network, wallet, wallet.DepositAddress)
|
||||
if err != nil {
|
||||
d.logger.Warn("Native balance failed",
|
||||
d.logger.Warn("native balance failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Native balance result",
|
||||
d.logger.Debug("native balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("amount", result.Amount),
|
||||
@@ -121,7 +121,7 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
|
||||
}
|
||||
|
||||
func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) {
|
||||
d.logger.Debug("Submit transfer request",
|
||||
d.logger.Debug("submit transfer request",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("destination", destination),
|
||||
@@ -130,13 +130,13 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
||||
driverDeps.Logger = d.logger
|
||||
txHash, err := evm.SubmitTransfer(ctx, driverDeps, network, transfer, source, source.DepositAddress, destination)
|
||||
if err != nil {
|
||||
d.logger.Warn("Submit transfer failed",
|
||||
d.logger.Warn("submit transfer failed",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
d.logger.Debug("Submit transfer result",
|
||||
d.logger.Debug("submit transfer result",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("tx_hash", txHash),
|
||||
@@ -146,7 +146,7 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
||||
}
|
||||
|
||||
func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
|
||||
d.logger.Debug("Await confirmation",
|
||||
d.logger.Debug("await confirmation",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
)
|
||||
@@ -154,13 +154,13 @@ func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, networ
|
||||
driverDeps.Logger = d.logger
|
||||
receipt, err := evm.AwaitConfirmation(ctx, driverDeps, network, txHash)
|
||||
if err != nil {
|
||||
d.logger.Warn("Await confirmation failed",
|
||||
d.logger.Warn("await confirmation failed",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if receipt != nil {
|
||||
d.logger.Debug("Await confirmation result",
|
||||
d.logger.Debug("await confirmation result",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
||||
|
||||
@@ -27,25 +27,25 @@ func (d *Driver) Name() string {
|
||||
}
|
||||
|
||||
func (d *Driver) FormatAddress(address string) (string, error) {
|
||||
d.logger.Debug("Format address", zap.String("address", address))
|
||||
d.logger.Debug("format address", zap.String("address", address))
|
||||
normalized, err := evm.NormalizeAddress(address)
|
||||
if err != nil {
|
||||
d.logger.Warn("Format address failed", zap.String("address", address), zap.Error(err))
|
||||
d.logger.Warn("format address failed", zap.String("address", address), zap.Error(err))
|
||||
}
|
||||
return normalized, err
|
||||
}
|
||||
|
||||
func (d *Driver) NormalizeAddress(address string) (string, error) {
|
||||
d.logger.Debug("Normalize address", zap.String("address", address))
|
||||
d.logger.Debug("normalize address", zap.String("address", address))
|
||||
normalized, err := evm.NormalizeAddress(address)
|
||||
if err != nil {
|
||||
d.logger.Warn("Normalize address failed", zap.String("address", address), zap.Error(err))
|
||||
d.logger.Warn("normalize address failed", zap.String("address", address), zap.Error(err))
|
||||
}
|
||||
return normalized, err
|
||||
}
|
||||
|
||||
func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||
d.logger.Debug("Balance request",
|
||||
d.logger.Debug("balance request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
)
|
||||
@@ -53,13 +53,13 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
||||
driverDeps.Logger = d.logger
|
||||
result, err := evm.Balance(ctx, driverDeps, network, wallet, wallet.DepositAddress)
|
||||
if err != nil {
|
||||
d.logger.Warn("Balance failed",
|
||||
d.logger.Warn("balance failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Balance result",
|
||||
d.logger.Debug("balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("amount", result.Amount),
|
||||
@@ -70,7 +70,7 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
||||
}
|
||||
|
||||
func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||
d.logger.Debug("Native balance request",
|
||||
d.logger.Debug("native balance request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
)
|
||||
@@ -78,13 +78,13 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
|
||||
driverDeps.Logger = d.logger
|
||||
result, err := evm.NativeBalance(ctx, driverDeps, network, wallet, wallet.DepositAddress)
|
||||
if err != nil {
|
||||
d.logger.Warn("Native balance failed",
|
||||
d.logger.Warn("native balance failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Native balance result",
|
||||
d.logger.Debug("native balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("amount", result.Amount),
|
||||
@@ -121,7 +121,7 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
|
||||
}
|
||||
|
||||
func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) {
|
||||
d.logger.Debug("Submit transfer request",
|
||||
d.logger.Debug("submit transfer request",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("destination", destination),
|
||||
@@ -130,13 +130,13 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
||||
driverDeps.Logger = d.logger
|
||||
txHash, err := evm.SubmitTransfer(ctx, driverDeps, network, transfer, source, source.DepositAddress, destination)
|
||||
if err != nil {
|
||||
d.logger.Warn("Submit transfer failed",
|
||||
d.logger.Warn("submit transfer failed",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
d.logger.Debug("Submit transfer result",
|
||||
d.logger.Debug("submit transfer result",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("tx_hash", txHash),
|
||||
@@ -146,7 +146,7 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
||||
}
|
||||
|
||||
func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
|
||||
d.logger.Debug("Await confirmation",
|
||||
d.logger.Debug("await confirmation",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
)
|
||||
@@ -154,13 +154,13 @@ func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, networ
|
||||
driverDeps.Logger = d.logger
|
||||
receipt, err := evm.AwaitConfirmation(ctx, driverDeps, network, txHash)
|
||||
if err != nil {
|
||||
d.logger.Warn("Await confirmation failed",
|
||||
d.logger.Warn("await confirmation failed",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if receipt != nil {
|
||||
d.logger.Debug("Await confirmation result",
|
||||
d.logger.Debug("await confirmation result",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
||||
|
||||
@@ -41,7 +41,7 @@ func (d *Driver) NormalizeAddress(address string) (string, error) {
|
||||
d.logger.Debug("Normalize address", zap.String("address", address))
|
||||
normalized, err := normalizeAddress(address)
|
||||
if err != nil {
|
||||
d.logger.Warn("Normalize address failed", zap.String("address", address), zap.Error(err))
|
||||
d.logger.Warn("normalize address failed", zap.String("address", address), zap.Error(err))
|
||||
}
|
||||
return normalized, err
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
||||
zap.String("network", network.Name),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Balance result",
|
||||
d.logger.Debug("balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("amount", result.Amount),
|
||||
@@ -100,7 +100,7 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
|
||||
zap.String("network", network.Name),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Native balance result",
|
||||
d.logger.Debug("native balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("amount", result.Amount),
|
||||
@@ -196,7 +196,7 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
||||
driverDeps.Logger = d.logger
|
||||
txHash, err := evm.SubmitTransfer(ctx, driverDeps, network, transfer, source, rpcFrom, rpcTo)
|
||||
if err != nil {
|
||||
d.logger.Warn("Submit transfer failed", zap.Error(err),
|
||||
d.logger.Warn("submit transfer failed", zap.Error(err),
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ func NewRegistry(logger mlogger.Logger, networks []shared.Network) (*Registry, e
|
||||
}
|
||||
chainDriver, err := resolveDriver(logger, name)
|
||||
if err != nil {
|
||||
logger.Error("Unsupported chain driver", zap.String("network", name), zap.Error(err))
|
||||
logger.Error("unsupported chain driver", zap.String("network", name), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
result.byNetwork[name] = chainDriver
|
||||
@@ -40,7 +40,7 @@ func NewRegistry(logger mlogger.Logger, networks []shared.Network) (*Registry, e
|
||||
if len(result.byNetwork) == 0 {
|
||||
return nil, merrors.InvalidArgument("driver registry: no supported networks configured")
|
||||
}
|
||||
logger.Info("Chain drivers configured", zap.Int("count", len(result.byNetwork)))
|
||||
logger.Info("chain drivers configured", zap.Int("count", len(result.byNetwork)))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -47,32 +47,32 @@ type onChainExecutor struct {
|
||||
|
||||
func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network shared.Network) (string, error) {
|
||||
if o.keyManager == nil {
|
||||
o.logger.Warn("Key manager not configured")
|
||||
o.logger.Warn("key manager not configured")
|
||||
return "", executorInternal("key manager is not configured", nil)
|
||||
}
|
||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||
if rpcURL == "" {
|
||||
o.logger.Warn("Network rpc url missing", zap.String("network", network.Name))
|
||||
o.logger.Warn("network rpc url missing", zap.String("network", network.Name))
|
||||
return "", executorInvalid("network rpc url is not configured")
|
||||
}
|
||||
if source == nil || transfer == nil {
|
||||
o.logger.Warn("Transfer context missing")
|
||||
o.logger.Warn("transfer context missing")
|
||||
return "", executorInvalid("transfer context missing")
|
||||
}
|
||||
if strings.TrimSpace(source.KeyReference) == "" {
|
||||
o.logger.Warn("Source wallet missing key reference", zap.String("wallet_ref", source.WalletRef))
|
||||
o.logger.Warn("source wallet missing key reference", zap.String("wallet_ref", source.WalletRef))
|
||||
return "", executorInvalid("source wallet missing key reference")
|
||||
}
|
||||
if strings.TrimSpace(source.DepositAddress) == "" {
|
||||
o.logger.Warn("Source wallet missing deposit address", zap.String("wallet_ref", source.WalletRef))
|
||||
o.logger.Warn("source wallet missing deposit address", zap.String("wallet_ref", source.WalletRef))
|
||||
return "", executorInvalid("source wallet missing deposit address")
|
||||
}
|
||||
if !common.IsHexAddress(destinationAddress) {
|
||||
o.logger.Warn("Invalid destination address", zap.String("transfer_ref", transfer.TransferRef), zap.String("address", destinationAddress))
|
||||
o.logger.Warn("invalid destination address", zap.String("transfer_ref", transfer.TransferRef), zap.String("address", destinationAddress))
|
||||
return "", executorInvalid("invalid destination address " + destinationAddress)
|
||||
}
|
||||
|
||||
o.logger.Info("Submitting transfer",
|
||||
o.logger.Info("submitting transfer",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("source_wallet_ref", source.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
@@ -162,7 +162,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
|
||||
|
||||
input, err := erc20ABI.Pack("transfer", destination, amountInt)
|
||||
if err != nil {
|
||||
o.logger.Warn("Failed to encode transfer call",
|
||||
o.logger.Warn("failed to encode transfer call",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
@@ -49,14 +49,14 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
|
||||
if rpcURL == "" {
|
||||
result.Close()
|
||||
err := merrors.InvalidArgument(fmt.Sprintf("rpc url not configured for network %s", name))
|
||||
clientLogger.Warn("Rpc url missing", zap.String("network", name))
|
||||
clientLogger.Warn("rpc url missing", zap.String("network", name))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields := []zap.Field{
|
||||
zap.String("network", name),
|
||||
}
|
||||
clientLogger.Info("Initialising rpc client", fields...)
|
||||
clientLogger.Info("initialising rpc client", fields...)
|
||||
|
||||
dialCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
httpClient := &http.Client{
|
||||
|
||||
@@ -41,7 +41,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
||||
}
|
||||
|
||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSigning, "", ""); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
s.logger.Warn("failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
}
|
||||
|
||||
driverDeps := s.driverDeps()
|
||||
@@ -81,7 +81,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
||||
}
|
||||
|
||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSubmitted, "", txHash); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
s.logger.Warn("failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
}
|
||||
|
||||
receiptCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
@@ -89,20 +89,20 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
||||
receipt, err := chainDriver.AwaitConfirmation(receiptCtx, driverDeps, network, txHash)
|
||||
if err != nil {
|
||||
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
|
||||
s.logger.Warn("Failed to await transfer confirmation", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
s.logger.Warn("failed to await transfer confirmation", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if receipt != nil && receipt.Status == types.ReceiptStatusSuccessful {
|
||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", txHash); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
s.logger.Warn("failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, "transaction reverted", txHash); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
s.logger.Warn("failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -28,10 +28,9 @@ type ServiceFee struct {
|
||||
}
|
||||
|
||||
type TransferDestination struct {
|
||||
ManagedWalletRef string `bson:"managedWalletRef,omitempty" json:"managedWalletRef,omitempty"`
|
||||
ExternalAddress string `bson:"externalAddress,omitempty" json:"externalAddress,omitempty"`
|
||||
ExternalAddressOriginal string `bson:"externalAddressOriginal,omitempty" json:"externalAddressOriginal,omitempty"`
|
||||
Memo string `bson:"memo,omitempty" json:"memo,omitempty"`
|
||||
ManagedWalletRef string `bson:"managedWalletRef,omitempty" json:"managedWalletRef,omitempty"`
|
||||
ExternalAddress string `bson:"externalAddress,omitempty" json:"externalAddress,omitempty"`
|
||||
Memo string `bson:"memo,omitempty" json:"memo,omitempty"`
|
||||
}
|
||||
|
||||
// Transfer models an on-chain transfer orchestrated by the gateway.
|
||||
@@ -86,8 +85,7 @@ func (t *Transfer) Normalize() {
|
||||
t.TokenSymbol = strings.TrimSpace(strings.ToUpper(t.TokenSymbol))
|
||||
t.ContractAddress = strings.TrimSpace(strings.ToLower(t.ContractAddress))
|
||||
t.Destination.ManagedWalletRef = strings.TrimSpace(t.Destination.ManagedWalletRef)
|
||||
t.Destination.ExternalAddress = normalizeWalletAddress(t.Destination.ExternalAddress)
|
||||
t.Destination.ExternalAddressOriginal = strings.TrimSpace(t.Destination.ExternalAddressOriginal)
|
||||
t.Destination.ExternalAddress = strings.TrimSpace(strings.ToLower(t.Destination.ExternalAddress))
|
||||
t.Destination.Memo = strings.TrimSpace(t.Destination.Memo)
|
||||
t.ClientReference = strings.TrimSpace(t.ClientReference)
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTransferNormalizePreservesBase58ExternalAddress(t *testing.T) {
|
||||
address := "TGBDXEg9rxSqGFJDcb889zqTjDwx1bmLRF"
|
||||
transfer := &Transfer{
|
||||
IdempotencyKey: "idemp",
|
||||
TransferRef: "ref",
|
||||
OrganizationRef: "org",
|
||||
SourceWalletRef: "wallet",
|
||||
Network: "tron_mainnet",
|
||||
TokenSymbol: "USDT",
|
||||
Destination: TransferDestination{
|
||||
ExternalAddress: address,
|
||||
ExternalAddressOriginal: address,
|
||||
},
|
||||
}
|
||||
|
||||
transfer.Normalize()
|
||||
|
||||
if transfer.Destination.ExternalAddress != address {
|
||||
t.Fatalf("expected external address to preserve case, got %q", transfer.Destination.ExternalAddress)
|
||||
}
|
||||
if transfer.Destination.ExternalAddressOriginal != address {
|
||||
t.Fatalf("expected external address original to preserve case, got %q", transfer.Destination.ExternalAddressOriginal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferNormalizeLowercasesHexExternalAddress(t *testing.T) {
|
||||
address := "0xAABBCCDDEEFF00112233445566778899AABBCCDD"
|
||||
transfer := &Transfer{
|
||||
Destination: TransferDestination{
|
||||
ExternalAddress: address,
|
||||
ExternalAddressOriginal: address,
|
||||
},
|
||||
}
|
||||
|
||||
transfer.Normalize()
|
||||
|
||||
if transfer.Destination.ExternalAddress != strings.ToLower(address) {
|
||||
t.Fatalf("expected hex external address to be lowercased, got %q", transfer.Destination.ExternalAddress)
|
||||
}
|
||||
if transfer.Destination.ExternalAddressOriginal != address {
|
||||
t.Fatalf("expected external address original to preserve case, got %q", transfer.Destination.ExternalAddressOriginal)
|
||||
}
|
||||
}
|
||||
@@ -44,23 +44,23 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
|
||||
defer cancel()
|
||||
|
||||
if err := result.Ping(ctx); err != nil {
|
||||
result.logger.Error("Mongo ping failed during repository initialisation", zap.Error(err))
|
||||
result.logger.Error("mongo ping failed during repository initialisation", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
walletsStore, err := store.NewWallets(result.logger, result.db)
|
||||
if err != nil {
|
||||
result.logger.Error("Failed to initialise wallets store", zap.Error(err))
|
||||
result.logger.Error("failed to initialise wallets store", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
transfersStore, err := store.NewTransfers(result.logger, result.db)
|
||||
if err != nil {
|
||||
result.logger.Error("Failed to initialise transfers store", zap.Error(err))
|
||||
result.logger.Error("failed to initialise transfers store", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
depositsStore, err := store.NewDeposits(result.logger, result.db)
|
||||
if err != nil {
|
||||
result.logger.Error("Failed to initialise deposits store", zap.Error(err))
|
||||
result.logger.Error("failed to initialise deposits store", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -48,13 +48,13 @@ func NewDeposits(logger mlogger.Logger, db *mongo.Database) (*Deposits, error) {
|
||||
}
|
||||
for _, def := range indexes {
|
||||
if err := repo.CreateIndex(def); err != nil {
|
||||
logger.Error("Failed to ensure deposit index", zap.Error(err), zap.String("collection", repo.Collection()))
|
||||
logger.Error("failed to ensure deposit index", zap.Error(err), zap.String("collection", repo.Collection()))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
childLogger := logger.Named("deposits")
|
||||
childLogger.Debug("Deposits store initialised")
|
||||
childLogger.Debug("deposits store initialised")
|
||||
|
||||
return &Deposits{logger: childLogger, repo: repo}, nil
|
||||
}
|
||||
|
||||
@@ -53,13 +53,13 @@ func NewTransfers(logger mlogger.Logger, db *mongo.Database) (*Transfers, error)
|
||||
}
|
||||
for _, def := range indexes {
|
||||
if err := repo.CreateIndex(def); err != nil {
|
||||
logger.Error("Failed to ensure transfer index", zap.Error(err), zap.String("collection", repo.Collection()))
|
||||
logger.Error("failed to ensure transfer index", zap.Error(err), zap.String("collection", repo.Collection()))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
childLogger := logger.Named("transfers")
|
||||
childLogger.Debug("Transfers store initialised")
|
||||
childLogger.Debug("transfers store initialised")
|
||||
|
||||
return &Transfers{
|
||||
logger: childLogger,
|
||||
@@ -89,12 +89,12 @@ func (t *Transfers) Create(ctx context.Context, transfer *model.Transfer) (*mode
|
||||
}
|
||||
if err := t.repo.Insert(ctx, transfer, repository.Filter("idempotencyKey", transfer.IdempotencyKey)); err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
t.logger.Debug("Transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", transfer.IdempotencyKey))
|
||||
t.logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", transfer.IdempotencyKey))
|
||||
return transfer, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
t.logger.Debug("Transfer created", zap.String("transfer_ref", transfer.TransferRef))
|
||||
t.logger.Debug("transfer created", zap.String("transfer_ref", transfer.TransferRef))
|
||||
return transfer, nil
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func (t *Transfers) List(ctx context.Context, filter model.TransferFilter) (*mod
|
||||
if oid, err := primitive.ObjectIDFromHex(cursor); err == nil {
|
||||
query = query.Comparison(repository.IDField(), builder.Gt, oid)
|
||||
} else {
|
||||
t.logger.Warn("Ignoring invalid transfer cursor", zap.String("cursor", cursor), zap.Error(err))
|
||||
t.logger.Warn("ignoring invalid transfer cursor", zap.String("cursor", cursor), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ func NewWallets(logger mlogger.Logger, db *mongo.Database) (*Wallets, error) {
|
||||
}
|
||||
for _, def := range walletIndexes {
|
||||
if err := walletRepo.CreateIndex(def); err != nil {
|
||||
logger.Error("Failed to ensure wallet index", zap.String("collection", walletRepo.Collection()), zap.Error(err))
|
||||
logger.Error("failed to ensure wallet index", zap.String("collection", walletRepo.Collection()), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -70,13 +70,13 @@ func NewWallets(logger mlogger.Logger, db *mongo.Database) (*Wallets, error) {
|
||||
}
|
||||
for _, def := range balanceIndexes {
|
||||
if err := balanceRepo.CreateIndex(def); err != nil {
|
||||
logger.Error("Failed to ensure wallet balance index", zap.String("collection", balanceRepo.Collection()), zap.Error(err))
|
||||
logger.Error("failed to ensure wallet balance index", zap.String("collection", balanceRepo.Collection()), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
childLogger := logger.Named("wallets")
|
||||
childLogger.Debug("Wallet stores initialised")
|
||||
childLogger.Debug("wallet stores initialised")
|
||||
|
||||
return &Wallets{
|
||||
logger: childLogger,
|
||||
@@ -117,13 +117,13 @@ func (w *Wallets) Create(ctx context.Context, wallet *model.ManagedWallet) (*mod
|
||||
}
|
||||
if err := w.walletRepo.Insert(ctx, wallet, repository.Filter("idempotencyKey", wallet.IdempotencyKey)); err != nil {
|
||||
if errors.Is(err, merrors.ErrDataConflict) {
|
||||
w.logger.Debug("Wallet already exists", fields...)
|
||||
w.logger.Debug("wallet already exists", fields...)
|
||||
return wallet, nil
|
||||
}
|
||||
w.logger.Warn("Wallet create failed", append(fields, zap.Error(err))...)
|
||||
w.logger.Warn("wallet create failed", append(fields, zap.Error(err))...)
|
||||
return nil, err
|
||||
}
|
||||
w.logger.Debug("Wallet created", fields...)
|
||||
w.logger.Debug("wallet created", fields...)
|
||||
return wallet, nil
|
||||
}
|
||||
|
||||
@@ -136,11 +136,11 @@ func (w *Wallets) Get(ctx context.Context, walletID string) (*model.ManagedWalle
|
||||
zap.String("wallet_id", walletID),
|
||||
}
|
||||
wallet := &model.ManagedWallet{}
|
||||
if err := w.walletRepo.FindOneByFilter(ctx, repository.Filter("walletRef", walletID), wallet); err != nil {
|
||||
if err := w.balanceRepo.FindOneByFilter(ctx, repository.Filter("walletRef", walletID), wallet); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
w.logger.Debug("Wallet not found", fields...)
|
||||
w.logger.Debug("wallet not found", fields...)
|
||||
} else {
|
||||
w.logger.Warn("Wallet lookup failed", append(fields, zap.Error(err))...)
|
||||
w.logger.Warn("wallet lookup failed", append(fields, zap.Error(err))...)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (*
|
||||
query = query.Comparison(repository.IDField(), builder.Gt, oid)
|
||||
fields = append(fields, zap.String("cursor", cursor))
|
||||
} else {
|
||||
w.logger.Warn("Ignoring invalid wallet cursor", zap.String("cursor", cursor), zap.Error(err))
|
||||
w.logger.Warn("ignoring invalid wallet cursor", zap.String("cursor", cursor), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (*
|
||||
|
||||
listErr := w.walletRepo.FindManyByFilter(ctx, query, decoder)
|
||||
if listErr != nil && !errors.Is(listErr, merrors.ErrNoData) {
|
||||
w.logger.Warn("Wallet list failed", append(fields, zap.Error(listErr))...)
|
||||
w.logger.Warn("wallet list failed", append(fields, zap.Error(listErr))...)
|
||||
return nil, listErr
|
||||
}
|
||||
|
||||
@@ -217,9 +217,9 @@ func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (*
|
||||
zap.String("next_cursor", result.NextCursor),
|
||||
)
|
||||
if errors.Is(listErr, merrors.ErrNoData) {
|
||||
w.logger.Debug("Wallet list empty", fields...)
|
||||
w.logger.Debug("wallet list empty", fields...)
|
||||
} else {
|
||||
w.logger.Debug("Wallet list fetched", fields...)
|
||||
w.logger.Debug("wallet list fetched", fields...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -246,20 +246,20 @@ func (w *Wallets) SaveBalance(ctx context.Context, balance *model.WalletBalance)
|
||||
existing.PendingOutbound = balance.PendingOutbound
|
||||
existing.CalculatedAt = balance.CalculatedAt
|
||||
if err := w.balanceRepo.Update(ctx, existing); err != nil {
|
||||
w.logger.Warn("Wallet balance update failed", append(fields, zap.Error(err))...)
|
||||
w.logger.Warn("wallet balance update failed", append(fields, zap.Error(err))...)
|
||||
return err
|
||||
}
|
||||
w.logger.Debug("Wallet balance updated", fields...)
|
||||
w.logger.Debug("wallet balance updated", fields...)
|
||||
return nil
|
||||
case errors.Is(err, merrors.ErrNoData):
|
||||
if err := w.balanceRepo.Insert(ctx, balance, repository.Filter("walletRef", balance.WalletRef)); err != nil {
|
||||
w.logger.Warn("Wallet balance create failed", append(fields, zap.Error(err))...)
|
||||
w.logger.Warn("wallet balance create failed", append(fields, zap.Error(err))...)
|
||||
return err
|
||||
}
|
||||
w.logger.Debug("Wallet balance created", fields...)
|
||||
w.logger.Debug("wallet balance created", fields...)
|
||||
return nil
|
||||
default:
|
||||
w.logger.Warn("Wallet balance lookup failed", append(fields, zap.Error(err))...)
|
||||
w.logger.Warn("wallet balance lookup failed", append(fields, zap.Error(err))...)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -273,13 +273,13 @@ func (w *Wallets) GetBalance(ctx context.Context, walletID string) (*model.Walle
|
||||
balance := &model.WalletBalance{}
|
||||
if err := w.balanceRepo.FindOneByFilter(ctx, repository.Filter("walletRef", walletID), balance); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
w.logger.Debug("Wallet balance not found", fields...)
|
||||
w.logger.Debug("wallet balance not found", fields...)
|
||||
} else {
|
||||
w.logger.Warn("Wallet balance lookup failed", append(fields, zap.Error(err))...)
|
||||
w.logger.Warn("wallet balance lookup failed", append(fields, zap.Error(err))...)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
w.logger.Debug("Wallet balance fetched", fields...)
|
||||
w.logger.Debug("wallet balance fetched", fields...)
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
@@ -24,7 +23,6 @@ type gatewayClient struct {
|
||||
conn *grpc.ClientConn
|
||||
client mntxv1.MntxGatewayServiceClient
|
||||
cfg Config
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// New dials the Monetix gateway.
|
||||
@@ -49,7 +47,6 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
||||
conn: conn,
|
||||
client: mntxv1.NewMntxGatewayServiceClient(conn),
|
||||
cfg: cfg,
|
||||
logger: cfg.Logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -60,39 +57,28 @@ func (g *gatewayClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *gatewayClient) callContext(ctx context.Context, method string) (context.Context, context.CancelFunc) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
func (g *gatewayClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
timeout := g.cfg.CallTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
fields := []zap.Field{
|
||||
zap.String("method", method),
|
||||
zap.Duration("timeout", timeout),
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
fields = append(fields, zap.Time("parent_deadline", deadline), zap.Duration("parent_deadline_in", time.Until(deadline)))
|
||||
}
|
||||
g.logger.Info("Mntx gateway client call timeout applied", fields...)
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
}
|
||||
|
||||
func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
||||
ctx, cancel := g.callContext(ctx, "CreateCardPayout")
|
||||
ctx, cancel := g.callContext(ctx)
|
||||
defer cancel()
|
||||
return g.client.CreateCardPayout(ctx, req)
|
||||
}
|
||||
|
||||
func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
|
||||
ctx, cancel := g.callContext(ctx, "CreateCardTokenPayout")
|
||||
ctx, cancel := g.callContext(ctx)
|
||||
defer cancel()
|
||||
return g.client.CreateCardTokenPayout(ctx, req)
|
||||
}
|
||||
|
||||
func (g *gatewayClient) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error) {
|
||||
ctx, cancel := g.callContext(ctx, "GetCardPayoutStatus")
|
||||
ctx, cancel := g.callContext(ctx)
|
||||
defer cancel()
|
||||
return g.client.GetCardPayoutStatus(ctx, req)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// Config holds Monetix gateway client settings.
|
||||
type Config struct {
|
||||
Address string
|
||||
DialTimeout time.Duration
|
||||
CallTimeout time.Duration
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
func (c *Config) setDefaults() {
|
||||
@@ -21,7 +16,4 @@ func (c *Config) setDefaults() {
|
||||
if c.CallTimeout <= 0 {
|
||||
c.CallTimeout = 10 * time.Second
|
||||
}
|
||||
if c.Logger == nil {
|
||||
c.Logger = zap.NewNop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,49 +95,22 @@ func (i *Imp) Shutdown() {
|
||||
}
|
||||
|
||||
func (i *Imp) Start() error {
|
||||
i.logger.Info("Starting Monetix gateway", zap.String("config_file", i.file), zap.Bool("debug", i.debug))
|
||||
|
||||
cfg, err := i.loadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.config = cfg
|
||||
|
||||
i.logger.Info("Configuration loaded",
|
||||
zap.String("grpc_address", cfg.GRPC.Address),
|
||||
zap.String("metrics_address", cfg.Metrics.Address),
|
||||
)
|
||||
|
||||
monetixCfg, err := i.resolveMonetixConfig(cfg.Monetix)
|
||||
if err != nil {
|
||||
i.logger.Error("Failed to resolve Monetix configuration", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
callbackCfg, err := i.resolveCallbackConfig(cfg.HTTP.Callback)
|
||||
if err != nil {
|
||||
i.logger.Error("Failed to resolve callback configuration", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
i.logger.Info("Monetix configuration resolved",
|
||||
zap.Bool("base_url_set", strings.TrimSpace(monetixCfg.BaseURL) != ""),
|
||||
zap.Int64("project_id", monetixCfg.ProjectID),
|
||||
zap.Bool("secret_key_set", strings.TrimSpace(monetixCfg.SecretKey) != ""),
|
||||
zap.Int("allowed_currencies", len(monetixCfg.AllowedCurrencies)),
|
||||
zap.Bool("require_customer_address", monetixCfg.RequireCustomerAddress),
|
||||
zap.Duration("request_timeout", monetixCfg.RequestTimeout),
|
||||
zap.String("status_success", monetixCfg.SuccessStatus()),
|
||||
zap.String("status_processing", monetixCfg.ProcessingStatus()),
|
||||
)
|
||||
|
||||
i.logger.Info("Callback configuration resolved",
|
||||
zap.String("address", callbackCfg.Address),
|
||||
zap.String("path", callbackCfg.Path),
|
||||
zap.Int("allowed_cidrs", len(callbackCfg.AllowedCIDRs)),
|
||||
zap.Int64("max_body_bytes", callbackCfg.MaxBodyBytes),
|
||||
)
|
||||
|
||||
serviceFactory := func(logger mlogger.Logger, _ struct{}, producer msg.Producer) (grpcapp.Service, error) {
|
||||
svc := mntxservice.NewService(logger,
|
||||
mntxservice.WithProducer(producer),
|
||||
@@ -164,7 +137,7 @@ func (i *Imp) Start() error {
|
||||
func (i *Imp) loadConfig() (*config, error) {
|
||||
data, err := os.ReadFile(i.file)
|
||||
if err != nil {
|
||||
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
|
||||
i.logger.Error("could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -172,7 +145,7 @@ func (i *Imp) loadConfig() (*config, error) {
|
||||
Config: &grpcapp.Config{},
|
||||
}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
i.logger.Error("Failed to parse configuration", zap.Error(err))
|
||||
i.logger.Error("failed to parse configuration", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -272,7 +245,7 @@ func (i *Imp) resolveCallbackConfig(cfg callbackConfig) (callbackRuntimeConfig,
|
||||
}
|
||||
_, block, err := net.ParseCIDR(clean)
|
||||
if err != nil {
|
||||
i.logger.Warn("Invalid callback allowlist CIDR skipped", zap.String("cidr", clean), zap.Error(err))
|
||||
i.logger.Warn("invalid callback allowlist CIDR skipped", zap.String("cidr", clean), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
cidrs = append(cidrs, block)
|
||||
@@ -297,36 +270,20 @@ func (i *Imp) startHTTPCallbackServer(svc *mntxservice.Service, cfg callbackRunt
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Post(cfg.Path, func(w http.ResponseWriter, r *http.Request) {
|
||||
log := i.logger.Named("callback_http")
|
||||
log.Debug("Callback request received",
|
||||
zap.String("remote_addr", strings.TrimSpace(r.RemoteAddr)),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
)
|
||||
|
||||
if len(cfg.AllowedCIDRs) > 0 && !clientAllowed(r, cfg.AllowedCIDRs) {
|
||||
ip := clientIPFromRequest(r)
|
||||
remoteIP := ""
|
||||
if ip != nil {
|
||||
remoteIP = ip.String()
|
||||
}
|
||||
log.Warn("Callback rejected by CIDR allowlist", zap.String("remote_ip", remoteIP))
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, cfg.MaxBodyBytes))
|
||||
if err != nil {
|
||||
log.Warn("Callback body read failed", zap.Error(err))
|
||||
http.Error(w, "failed to read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
status, err := svc.ProcessMonetixCallback(r.Context(), body)
|
||||
if err != nil {
|
||||
log.Warn("Callback processing failed", zap.Error(err), zap.Int("status", status))
|
||||
http.Error(w, err.Error(), status)
|
||||
return
|
||||
}
|
||||
log.Debug("Callback processed", zap.Int("status", status))
|
||||
w.WriteHeader(status)
|
||||
})
|
||||
|
||||
@@ -344,7 +301,7 @@ func (i *Imp) startHTTPCallbackServer(svc *mntxservice.Service, cfg callbackRunt
|
||||
|
||||
go func() {
|
||||
if err := server.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
i.logger.Warn("Monetix callback server stopped with error", zap.Error(err))
|
||||
i.logger.Error("Monetix callback server stopped with error", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientIPFromRequest(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: http.Header{"X-Forwarded-For": []string{"1.2.3.4, 5.6.7.8"}},
|
||||
RemoteAddr: "9.8.7.6:1234",
|
||||
}
|
||||
ip := clientIPFromRequest(req)
|
||||
if ip == nil || ip.String() != "1.2.3.4" {
|
||||
t.Fatalf("expected forwarded ip, got %v", ip)
|
||||
}
|
||||
|
||||
req = &http.Request{RemoteAddr: "9.8.7.6:1234"}
|
||||
ip = clientIPFromRequest(req)
|
||||
if ip == nil || ip.String() != "9.8.7.6" {
|
||||
t.Fatalf("expected remote addr ip, got %v", ip)
|
||||
}
|
||||
|
||||
req = &http.Request{RemoteAddr: "invalid"}
|
||||
ip = clientIPFromRequest(req)
|
||||
if ip != nil {
|
||||
t.Fatalf("expected nil ip, got %v", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAllowed(t *testing.T) {
|
||||
_, cidr, err := net.ParseCIDR("10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse cidr: %v", err)
|
||||
}
|
||||
|
||||
allowedReq := &http.Request{RemoteAddr: "10.1.2.3:1234"}
|
||||
if !clientAllowed(allowedReq, []*net.IPNet{cidr}) {
|
||||
t.Fatalf("expected allowed request")
|
||||
}
|
||||
|
||||
deniedReq := &http.Request{RemoteAddr: "8.8.8.8:1234"}
|
||||
if clientAllowed(deniedReq, []*net.IPNet{cidr}) {
|
||||
t.Fatalf("expected denied request")
|
||||
}
|
||||
|
||||
openReq := &http.Request{RemoteAddr: "8.8.8.8:1234"}
|
||||
if !clientAllowed(openReq, nil) {
|
||||
t.Fatalf("expected allow when no cidrs are configured")
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
@@ -67,12 +66,9 @@ type monetixCallback struct {
|
||||
|
||||
// ProcessMonetixCallback ingests Monetix provider callbacks and updates payout state.
|
||||
func (s *Service) ProcessMonetixCallback(ctx context.Context, payload []byte) (int, error) {
|
||||
log := s.logger.Named("callback")
|
||||
if s.card == nil {
|
||||
log.Warn("Card payout processor not initialised")
|
||||
return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
log.Debug("Callback processing requested", zap.Int("payload_bytes", len(payload)))
|
||||
return s.card.ProcessCallback(ctx, payload)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
)
|
||||
|
||||
type fixedClock struct {
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func (f fixedClock) Now() time.Time {
|
||||
return f.now
|
||||
}
|
||||
|
||||
func baseCallback() monetixCallback {
|
||||
cb := monetixCallback{
|
||||
ProjectID: 42,
|
||||
}
|
||||
cb.Payment.ID = "payout-1"
|
||||
cb.Payment.Status = "success"
|
||||
cb.Payment.Sum.Amount = 5000
|
||||
cb.Payment.Sum.Currency = "usd"
|
||||
cb.Customer.ID = "cust-1"
|
||||
cb.Operation.Status = "success"
|
||||
cb.Operation.Code = ""
|
||||
cb.Operation.Message = "ok"
|
||||
cb.Operation.RequestID = "req-1"
|
||||
cb.Operation.Provider.PaymentID = "prov-1"
|
||||
return cb
|
||||
}
|
||||
|
||||
func TestMapCallbackToState_StatusMapping(t *testing.T) {
|
||||
now := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
cfg := monetix.DefaultConfig()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
paymentStatus string
|
||||
operationStatus string
|
||||
code string
|
||||
expectedStatus mntxv1.PayoutStatus
|
||||
expectedOutcome string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
paymentStatus: "success",
|
||||
operationStatus: "success",
|
||||
code: "0",
|
||||
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED,
|
||||
expectedOutcome: monetix.OutcomeSuccess,
|
||||
},
|
||||
{
|
||||
name: "processing",
|
||||
paymentStatus: "processing",
|
||||
operationStatus: "success",
|
||||
code: "",
|
||||
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
||||
expectedOutcome: monetix.OutcomeProcessing,
|
||||
},
|
||||
{
|
||||
name: "decline",
|
||||
paymentStatus: "failed",
|
||||
operationStatus: "failed",
|
||||
code: "1",
|
||||
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
|
||||
expectedOutcome: monetix.OutcomeDecline,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cb := baseCallback()
|
||||
cb.Payment.Status = tc.paymentStatus
|
||||
cb.Operation.Status = tc.operationStatus
|
||||
cb.Operation.Code = tc.code
|
||||
|
||||
state, outcome := mapCallbackToState(fixedClock{now: now}, cfg, cb)
|
||||
if state.Status != tc.expectedStatus {
|
||||
t.Fatalf("expected status %v, got %v", tc.expectedStatus, state.Status)
|
||||
}
|
||||
if outcome != tc.expectedOutcome {
|
||||
t.Fatalf("expected outcome %q, got %q", tc.expectedOutcome, outcome)
|
||||
}
|
||||
if state.Currency != "USD" {
|
||||
t.Fatalf("expected currency USD, got %q", state.Currency)
|
||||
}
|
||||
if !state.UpdatedAt.AsTime().Equal(now) {
|
||||
t.Fatalf("expected updated_at %v, got %v", now, state.UpdatedAt.AsTime())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackProviderPaymentID(t *testing.T) {
|
||||
cb := baseCallback()
|
||||
if got := fallbackProviderPaymentID(cb); got != "prov-1" {
|
||||
t.Fatalf("expected provider payment id, got %q", got)
|
||||
}
|
||||
cb.Operation.Provider.PaymentID = ""
|
||||
if got := fallbackProviderPaymentID(cb); got != "req-1" {
|
||||
t.Fatalf("expected request id fallback, got %q", got)
|
||||
}
|
||||
cb.Operation.RequestID = ""
|
||||
if got := fallbackProviderPaymentID(cb); got != "payout-1" {
|
||||
t.Fatalf("expected payment id fallback, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyCallbackSignature(t *testing.T) {
|
||||
secret := "secret"
|
||||
cb := baseCallback()
|
||||
|
||||
sig, err := monetix.SignPayload(cb, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign payload: %v", err)
|
||||
}
|
||||
cb.Signature = sig
|
||||
if err := verifyCallbackSignature(cb, secret); err != nil {
|
||||
t.Fatalf("expected valid signature, got %v", err)
|
||||
}
|
||||
|
||||
cb.Signature = "invalid"
|
||||
if err := verifyCallbackSignature(cb, secret); err == nil {
|
||||
t.Fatalf("expected signature mismatch error")
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -18,24 +17,14 @@ func (s *Service) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRe
|
||||
}
|
||||
|
||||
func (s *Service) handleCreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) gsresponse.Responder[mntxv1.CardPayoutResponse] {
|
||||
log := s.logger.Named("card_payout")
|
||||
log.Info("Create card payout request received",
|
||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||
)
|
||||
if s.card == nil {
|
||||
log.Warn("Card payout processor not initialised")
|
||||
return gsresponse.Internal[mntxv1.CardPayoutResponse](s.logger, mservice.MntxGateway, merrors.Internal("card payout processor not initialised"))
|
||||
}
|
||||
|
||||
resp, err := s.card.Submit(ctx, req)
|
||||
if err != nil {
|
||||
log.Warn("Card payout submission failed", zap.Error(err))
|
||||
return gsresponse.Auto[mntxv1.CardPayoutResponse](s.logger, mservice.MntxGateway, err)
|
||||
}
|
||||
log.Info("Card payout submission completed", zap.String("payout_id", resp.GetPayout().GetPayoutId()), zap.Bool("accepted", resp.GetAccepted()))
|
||||
return gsresponse.Success(resp)
|
||||
}
|
||||
|
||||
@@ -44,24 +33,14 @@ func (s *Service) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTok
|
||||
}
|
||||
|
||||
func (s *Service) handleCreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) gsresponse.Responder[mntxv1.CardTokenPayoutResponse] {
|
||||
log := s.logger.Named("card_token_payout")
|
||||
log.Info("Create card token payout request received",
|
||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||
)
|
||||
if s.card == nil {
|
||||
log.Warn("Card payout processor not initialised")
|
||||
return gsresponse.Internal[mntxv1.CardTokenPayoutResponse](s.logger, mservice.MntxGateway, merrors.Internal("card payout processor not initialised"))
|
||||
}
|
||||
|
||||
resp, err := s.card.SubmitToken(ctx, req)
|
||||
if err != nil {
|
||||
log.Warn("Card token payout submission failed", zap.Error(err))
|
||||
return gsresponse.Auto[mntxv1.CardTokenPayoutResponse](s.logger, mservice.MntxGateway, err)
|
||||
}
|
||||
log.Info("Card token payout submission completed", zap.String("payout_id", resp.GetPayout().GetPayoutId()), zap.Bool("accepted", resp.GetAccepted()))
|
||||
return gsresponse.Success(resp)
|
||||
}
|
||||
|
||||
@@ -70,22 +49,14 @@ func (s *Service) CreateCardToken(ctx context.Context, req *mntxv1.CardTokenizeR
|
||||
}
|
||||
|
||||
func (s *Service) handleCreateCardToken(ctx context.Context, req *mntxv1.CardTokenizeRequest) gsresponse.Responder[mntxv1.CardTokenizeResponse] {
|
||||
log := s.logger.Named("card_tokenize")
|
||||
log.Info("Create card token request received",
|
||||
zap.String("request_id", strings.TrimSpace(req.GetRequestId())),
|
||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||
)
|
||||
if s.card == nil {
|
||||
log.Warn("Card payout processor not initialised")
|
||||
return gsresponse.Internal[mntxv1.CardTokenizeResponse](s.logger, mservice.MntxGateway, merrors.Internal("card payout processor not initialised"))
|
||||
}
|
||||
|
||||
resp, err := s.card.Tokenize(ctx, req)
|
||||
if err != nil {
|
||||
log.Warn("Card tokenization failed", zap.Error(err))
|
||||
return gsresponse.Auto[mntxv1.CardTokenizeResponse](s.logger, mservice.MntxGateway, err)
|
||||
}
|
||||
log.Info("Card tokenization completed", zap.String("request_id", resp.GetRequestId()), zap.Bool("success", resp.GetSuccess()))
|
||||
return gsresponse.Success(resp)
|
||||
}
|
||||
|
||||
@@ -94,19 +65,14 @@ func (s *Service) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPa
|
||||
}
|
||||
|
||||
func (s *Service) handleGetCardPayoutStatus(_ context.Context, req *mntxv1.GetCardPayoutStatusRequest) gsresponse.Responder[mntxv1.GetCardPayoutStatusResponse] {
|
||||
log := s.logger.Named("card_payout_status")
|
||||
log.Info("Get card payout status request received", zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())))
|
||||
if s.card == nil {
|
||||
log.Warn("Card payout processor not initialised")
|
||||
return gsresponse.Internal[mntxv1.GetCardPayoutStatusResponse](s.logger, mservice.MntxGateway, merrors.Internal("card payout processor not initialised"))
|
||||
}
|
||||
|
||||
state, err := s.card.Status(context.Background(), req.GetPayoutId())
|
||||
if err != nil {
|
||||
log.Warn("Card payout status lookup failed", zap.Error(err))
|
||||
return gsresponse.Auto[mntxv1.GetCardPayoutStatusResponse](s.logger, mservice.MntxGateway, err)
|
||||
}
|
||||
log.Info("Card payout status retrieved", zap.String("payout_id", state.GetPayoutId()), zap.String("status", state.GetStatus().String()))
|
||||
return gsresponse.Success(&mntxv1.GetCardPayoutStatusResponse{Payout: state})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
)
|
||||
|
||||
func TestValidateCardPayoutRequest_Valid(t *testing.T) {
|
||||
cfg := testMonetixConfig()
|
||||
req := validCardPayoutRequest()
|
||||
if err := validateCardPayoutRequest(req, cfg); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCardPayoutRequest_Errors(t *testing.T) {
|
||||
baseCfg := testMonetixConfig()
|
||||
cases := []struct {
|
||||
name string
|
||||
mutate func(*mntxv1.CardPayoutRequest)
|
||||
config func(monetix.Config) monetix.Config
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "missing_payout_id",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.PayoutId = "" },
|
||||
expected: "missing_payout_id",
|
||||
},
|
||||
{
|
||||
name: "missing_customer_id",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CustomerId = "" },
|
||||
expected: "missing_customer_id",
|
||||
},
|
||||
{
|
||||
name: "missing_customer_ip",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CustomerIp = "" },
|
||||
expected: "missing_customer_ip",
|
||||
},
|
||||
{
|
||||
name: "invalid_amount",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.AmountMinor = 0 },
|
||||
expected: "invalid_amount",
|
||||
},
|
||||
{
|
||||
name: "missing_currency",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.Currency = "" },
|
||||
expected: "missing_currency",
|
||||
},
|
||||
{
|
||||
name: "unsupported_currency",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.Currency = "EUR" },
|
||||
config: func(cfg monetix.Config) monetix.Config {
|
||||
cfg.AllowedCurrencies = []string{"USD"}
|
||||
return cfg
|
||||
},
|
||||
expected: "unsupported_currency",
|
||||
},
|
||||
{
|
||||
name: "missing_card_pan",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardPan = "" },
|
||||
expected: "missing_card_pan",
|
||||
},
|
||||
{
|
||||
name: "missing_card_holder",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardHolder = "" },
|
||||
expected: "missing_card_holder",
|
||||
},
|
||||
{
|
||||
name: "invalid_expiry_month",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardExpMonth = 13 },
|
||||
expected: "invalid_expiry_month",
|
||||
},
|
||||
{
|
||||
name: "invalid_expiry_year",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardExpYear = 0 },
|
||||
expected: "invalid_expiry_year",
|
||||
},
|
||||
{
|
||||
name: "missing_customer_country_when_required",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CustomerCountry = "" },
|
||||
config: func(cfg monetix.Config) monetix.Config {
|
||||
cfg.RequireCustomerAddress = true
|
||||
return cfg
|
||||
},
|
||||
expected: "missing_customer_country",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := validCardPayoutRequest()
|
||||
tc.mutate(req)
|
||||
cfg := baseCfg
|
||||
if tc.config != nil {
|
||||
cfg = tc.config(cfg)
|
||||
}
|
||||
err := validateCardPayoutRequest(req, cfg)
|
||||
requireReason(t, err, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -45,20 +45,14 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
p.logger.Info("Submitting card payout",
|
||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||
)
|
||||
if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
|
||||
p.logger.Warn("Monetix configuration is incomplete for payout submission")
|
||||
p.logger.Warn("monetix configuration is incomplete for payout submission")
|
||||
return nil, merrors.Internal("monetix configuration is incomplete")
|
||||
}
|
||||
|
||||
req = sanitizeCardPayoutRequest(req)
|
||||
if err := validateCardPayoutRequest(req, p.config); err != nil {
|
||||
p.logger.Warn("Card payout validation failed",
|
||||
p.logger.Warn("card payout validation failed",
|
||||
zap.String("payout_id", req.GetPayoutId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
zap.Error(err),
|
||||
@@ -71,7 +65,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
||||
projectID = p.config.ProjectID
|
||||
}
|
||||
if projectID == 0 {
|
||||
p.logger.Warn("Monetix project_id is not configured", zap.String("payout_id", req.GetPayoutId()))
|
||||
p.logger.Warn("monetix project_id is not configured", zap.String("payout_id", req.GetPayoutId()))
|
||||
return nil, merrors.Internal("monetix project_id is not configured")
|
||||
}
|
||||
|
||||
@@ -101,7 +95,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
||||
state.ProviderMessage = err.Error()
|
||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
||||
p.store.Save(state)
|
||||
p.logger.Warn("Monetix payout submission failed",
|
||||
p.logger.Warn("monetix payout submission failed",
|
||||
zap.String("payout_id", req.GetPayoutId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
zap.Error(err),
|
||||
@@ -128,13 +122,6 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
||||
ErrorMessage: result.ErrorMessage,
|
||||
}
|
||||
|
||||
p.logger.Info("Card payout submission stored",
|
||||
zap.String("payout_id", state.GetPayoutId()),
|
||||
zap.String("status", state.GetStatus().String()),
|
||||
zap.Bool("accepted", result.Accepted),
|
||||
zap.String("provider_request_id", result.ProviderRequestID),
|
||||
)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -142,20 +129,14 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
p.logger.Info("Submitting card token payout",
|
||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||
)
|
||||
if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
|
||||
p.logger.Warn("Monetix configuration is incomplete for token payout submission")
|
||||
p.logger.Warn("monetix configuration is incomplete for token payout submission")
|
||||
return nil, merrors.Internal("monetix configuration is incomplete")
|
||||
}
|
||||
|
||||
req = sanitizeCardTokenPayoutRequest(req)
|
||||
if err := validateCardTokenPayoutRequest(req, p.config); err != nil {
|
||||
p.logger.Warn("Card token payout validation failed",
|
||||
p.logger.Warn("card token payout validation failed",
|
||||
zap.String("payout_id", req.GetPayoutId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
zap.Error(err),
|
||||
@@ -168,7 +149,7 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
||||
projectID = p.config.ProjectID
|
||||
}
|
||||
if projectID == 0 {
|
||||
p.logger.Warn("Monetix project_id is not configured", zap.String("payout_id", req.GetPayoutId()))
|
||||
p.logger.Warn("monetix project_id is not configured", zap.String("payout_id", req.GetPayoutId()))
|
||||
return nil, merrors.Internal("monetix project_id is not configured")
|
||||
}
|
||||
|
||||
@@ -198,7 +179,7 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
||||
state.ProviderMessage = err.Error()
|
||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
||||
p.store.Save(state)
|
||||
p.logger.Warn("Monetix token payout submission failed",
|
||||
p.logger.Warn("monetix token payout submission failed",
|
||||
zap.String("payout_id", req.GetPayoutId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
zap.Error(err),
|
||||
@@ -225,13 +206,6 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
||||
ErrorMessage: result.ErrorMessage,
|
||||
}
|
||||
|
||||
p.logger.Info("Card token payout submission stored",
|
||||
zap.String("payout_id", state.GetPayoutId()),
|
||||
zap.String("status", state.GetStatus().String()),
|
||||
zap.Bool("accepted", result.Accepted),
|
||||
zap.String("provider_request_id", result.ProviderRequestID),
|
||||
)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -239,13 +213,9 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
p.logger.Info("Submitting card tokenization",
|
||||
zap.String("request_id", strings.TrimSpace(req.GetRequestId())),
|
||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||
)
|
||||
cardInput, err := validateCardTokenizeRequest(req, p.config)
|
||||
if err != nil {
|
||||
p.logger.Warn("Card tokenization validation failed",
|
||||
p.logger.Warn("card tokenization validation failed",
|
||||
zap.String("request_id", req.GetRequestId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
zap.Error(err),
|
||||
@@ -258,7 +228,7 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
||||
projectID = p.config.ProjectID
|
||||
}
|
||||
if projectID == 0 {
|
||||
p.logger.Warn("Monetix project_id is not configured", zap.String("request_id", req.GetRequestId()))
|
||||
p.logger.Warn("monetix project_id is not configured", zap.String("request_id", req.GetRequestId()))
|
||||
return nil, merrors.Internal("monetix project_id is not configured")
|
||||
}
|
||||
|
||||
@@ -268,7 +238,7 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
||||
apiReq := buildCardTokenizeRequest(projectID, req, cardInput)
|
||||
result, err := client.CreateCardTokenization(ctx, apiReq)
|
||||
if err != nil {
|
||||
p.logger.Warn("Monetix tokenization request failed",
|
||||
p.logger.Warn("monetix tokenization request failed",
|
||||
zap.String("request_id", req.GetRequestId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
zap.Error(err),
|
||||
@@ -288,12 +258,6 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
||||
resp.ExpiryYear = result.ExpiryYear
|
||||
resp.CardBrand = result.CardBrand
|
||||
|
||||
p.logger.Info("Card tokenization completed",
|
||||
zap.String("request_id", resp.GetRequestId()),
|
||||
zap.Bool("success", resp.GetSuccess()),
|
||||
zap.String("provider_request_id", result.ProviderRequestID),
|
||||
)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -303,18 +267,16 @@ func (p *cardPayoutProcessor) Status(_ context.Context, payoutID string) (*mntxv
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(payoutID)
|
||||
p.logger.Info("Card payout status requested", zap.String("payout_id", id))
|
||||
if id == "" {
|
||||
p.logger.Warn("Payout status requested with empty payout_id")
|
||||
p.logger.Warn("payout status requested with empty payout_id")
|
||||
return nil, merrors.InvalidArgument("payout_id is required", "payout_id")
|
||||
}
|
||||
|
||||
state, ok := p.store.Get(id)
|
||||
if !ok || state == nil {
|
||||
p.logger.Warn("Payout status not found", zap.String("payout_id", id))
|
||||
p.logger.Warn("payout status not found", zap.String("payout_id", id))
|
||||
return nil, merrors.NoData("payout not found")
|
||||
}
|
||||
p.logger.Info("Card payout status resolved", zap.String("payout_id", state.GetPayoutId()), zap.String("status", state.GetStatus().String()))
|
||||
return state, nil
|
||||
}
|
||||
|
||||
@@ -322,19 +284,18 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt
|
||||
if p == nil {
|
||||
return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
p.logger.Debug("Processing Monetix callback", zap.Int("payload_bytes", len(payload)))
|
||||
if len(payload) == 0 {
|
||||
p.logger.Warn("Received empty Monetix callback payload")
|
||||
p.logger.Warn("received empty Monetix callback payload")
|
||||
return http.StatusBadRequest, merrors.InvalidArgument("callback body is empty")
|
||||
}
|
||||
if strings.TrimSpace(p.config.SecretKey) == "" {
|
||||
p.logger.Warn("Monetix secret key is not configured; cannot verify callback")
|
||||
p.logger.Warn("monetix secret key is not configured; cannot verify callback")
|
||||
return http.StatusInternalServerError, merrors.Internal("monetix secret key is not configured")
|
||||
}
|
||||
|
||||
var cb monetixCallback
|
||||
if err := json.Unmarshal(payload, &cb); err != nil {
|
||||
p.logger.Warn("Failed to unmarshal Monetix callback", zap.Error(err))
|
||||
p.logger.Warn("failed to unmarshal Monetix callback", zap.Error(err))
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
@@ -343,12 +304,7 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt
|
||||
return http.StatusBadRequest, merrors.InvalidArgument("signature is missing")
|
||||
}
|
||||
if err := verifyCallbackSignature(cb, p.config.SecretKey); err != nil {
|
||||
p.logger.Warn("Monetix callback signature check failed",
|
||||
zap.String("payout_id", cb.Payment.ID),
|
||||
zap.String("signature", cb.Signature),
|
||||
zap.String("payload", string(payload)),
|
||||
zap.Error(err),
|
||||
)
|
||||
p.logger.Warn("Monetix callback signature check failed", zap.Error(err))
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
|
||||
@@ -381,16 +337,16 @@ func (p *cardPayoutProcessor) emitCardPayoutEvent(state *mntxv1.CardPayoutState)
|
||||
event := &mntxv1.CardPayoutStatusChangedEvent{Payout: state}
|
||||
payload, err := protojson.Marshal(event)
|
||||
if err != nil {
|
||||
p.logger.Warn("Failed to marshal payout callback event", zap.Error(err))
|
||||
p.logger.Warn("failed to marshal payout callback event", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
env := messaging.CreateEnvelope(string(mservice.MntxGateway), model.NewNotification(mservice.MntxGateway, nm.NAUpdated))
|
||||
if _, err := env.Wrap(payload); err != nil {
|
||||
p.logger.Warn("Failed to wrap payout callback event payload", zap.Error(err))
|
||||
p.logger.Warn("failed to wrap payout callback event payload", zap.Error(err))
|
||||
return
|
||||
}
|
||||
if err := p.producer.SendMessage(env); err != nil {
|
||||
p.logger.Warn("Failed to publish payout callback event", zap.Error(err))
|
||||
p.logger.Warn("failed to publish payout callback event", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return f(r)
|
||||
}
|
||||
|
||||
type staticClock struct {
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func (s staticClock) Now() time.Time {
|
||||
return s.now
|
||||
}
|
||||
|
||||
func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
|
||||
cfg := monetix.Config{
|
||||
BaseURL: "https://monetix.test",
|
||||
SecretKey: "secret",
|
||||
ProjectID: 99,
|
||||
AllowedCurrencies: []string{"RUB"},
|
||||
}
|
||||
|
||||
existingCreated := timestamppb.New(time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC))
|
||||
store := newCardPayoutStore()
|
||||
store.Save(&mntxv1.CardPayoutState{
|
||||
PayoutId: "payout-1",
|
||||
CreatedAt: existingCreated,
|
||||
})
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||
resp := monetix.APIResponse{}
|
||||
resp.Operation.RequestID = "req-123"
|
||||
body, _ := json.Marshal(resp)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||
processor := newCardPayoutProcessor(zap.NewNop(), cfg, staticClock{now: now}, store, httpClient, nil)
|
||||
|
||||
req := validCardPayoutRequest()
|
||||
req.ProjectId = 0
|
||||
|
||||
resp, err := processor.Submit(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if !resp.GetAccepted() {
|
||||
t.Fatalf("expected accepted payout response")
|
||||
}
|
||||
if resp.GetPayout().GetProjectId() != cfg.ProjectID {
|
||||
t.Fatalf("expected project id %d, got %d", cfg.ProjectID, resp.GetPayout().GetProjectId())
|
||||
}
|
||||
if resp.GetPayout().GetStatus() != mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING {
|
||||
t.Fatalf("expected pending status, got %v", resp.GetPayout().GetStatus())
|
||||
}
|
||||
if !resp.GetPayout().GetCreatedAt().AsTime().Equal(existingCreated.AsTime()) {
|
||||
t.Fatalf("expected created_at preserved, got %v", resp.GetPayout().GetCreatedAt().AsTime())
|
||||
}
|
||||
|
||||
stored, ok := store.Get(req.GetPayoutId())
|
||||
if !ok || stored == nil {
|
||||
t.Fatalf("expected payout state stored")
|
||||
}
|
||||
if stored.GetProviderPaymentId() == "" {
|
||||
t.Fatalf("expected provider payment id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardPayoutProcessor_Submit_MissingConfig(t *testing.T) {
|
||||
cfg := monetix.Config{
|
||||
AllowedCurrencies: []string{"RUB"},
|
||||
}
|
||||
processor := newCardPayoutProcessor(zap.NewNop(), cfg, clockpkg.NewSystem(), newCardPayoutStore(), &http.Client{}, nil)
|
||||
|
||||
_, err := processor.Submit(context.Background(), validCardPayoutRequest())
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !errors.Is(err, merrors.ErrInternal) {
|
||||
t.Fatalf("expected internal error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardPayoutProcessor_ProcessCallback(t *testing.T) {
|
||||
cfg := monetix.Config{
|
||||
SecretKey: "secret",
|
||||
StatusSuccess: "success",
|
||||
StatusProcessing: "processing",
|
||||
AllowedCurrencies: []string{"RUB"},
|
||||
}
|
||||
store := newCardPayoutStore()
|
||||
processor := newCardPayoutProcessor(zap.NewNop(), cfg, staticClock{now: time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)}, store, &http.Client{}, nil)
|
||||
|
||||
cb := baseCallback()
|
||||
cb.Payment.Sum.Currency = "RUB"
|
||||
cb.Signature = ""
|
||||
sig, err := monetix.SignPayload(cb, cfg.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign callback: %v", err)
|
||||
}
|
||||
cb.Signature = sig
|
||||
|
||||
payload, err := json.Marshal(cb)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal callback: %v", err)
|
||||
}
|
||||
|
||||
status, err := processor.ProcessCallback(context.Background(), payload)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected status ok, got %d", status)
|
||||
}
|
||||
|
||||
state, ok := store.Get(cb.Payment.ID)
|
||||
if !ok || state == nil {
|
||||
t.Fatalf("expected payout state stored")
|
||||
}
|
||||
if state.GetStatus() != mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED {
|
||||
t.Fatalf("expected processed status, got %v", state.GetStatus())
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
)
|
||||
|
||||
func TestValidateCardTokenPayoutRequest_Valid(t *testing.T) {
|
||||
cfg := testMonetixConfig()
|
||||
req := validCardTokenPayoutRequest()
|
||||
if err := validateCardTokenPayoutRequest(req, cfg); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCardTokenPayoutRequest_Errors(t *testing.T) {
|
||||
baseCfg := testMonetixConfig()
|
||||
cases := []struct {
|
||||
name string
|
||||
mutate func(*mntxv1.CardTokenPayoutRequest)
|
||||
config func(monetix.Config) monetix.Config
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "missing_payout_id",
|
||||
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.PayoutId = "" },
|
||||
expected: "missing_payout_id",
|
||||
},
|
||||
{
|
||||
name: "missing_customer_id",
|
||||
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.CustomerId = "" },
|
||||
expected: "missing_customer_id",
|
||||
},
|
||||
{
|
||||
name: "missing_customer_ip",
|
||||
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.CustomerIp = "" },
|
||||
expected: "missing_customer_ip",
|
||||
},
|
||||
{
|
||||
name: "invalid_amount",
|
||||
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.AmountMinor = 0 },
|
||||
expected: "invalid_amount",
|
||||
},
|
||||
{
|
||||
name: "missing_currency",
|
||||
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.Currency = "" },
|
||||
expected: "missing_currency",
|
||||
},
|
||||
{
|
||||
name: "unsupported_currency",
|
||||
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.Currency = "EUR" },
|
||||
config: func(cfg monetix.Config) monetix.Config {
|
||||
cfg.AllowedCurrencies = []string{"USD"}
|
||||
return cfg
|
||||
},
|
||||
expected: "unsupported_currency",
|
||||
},
|
||||
{
|
||||
name: "missing_card_token",
|
||||
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.CardToken = "" },
|
||||
expected: "missing_card_token",
|
||||
},
|
||||
{
|
||||
name: "missing_customer_city_when_required",
|
||||
mutate: func(r *mntxv1.CardTokenPayoutRequest) {
|
||||
r.CustomerCountry = "US"
|
||||
r.CustomerCity = ""
|
||||
r.CustomerAddress = "Main St"
|
||||
r.CustomerZip = "12345"
|
||||
},
|
||||
config: func(cfg monetix.Config) monetix.Config {
|
||||
cfg.RequireCustomerAddress = true
|
||||
return cfg
|
||||
},
|
||||
expected: "missing_customer_city",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := validCardTokenPayoutRequest()
|
||||
tc.mutate(req)
|
||||
cfg := baseCfg
|
||||
if tc.config != nil {
|
||||
cfg = tc.config(cfg)
|
||||
}
|
||||
err := validateCardTokenPayoutRequest(req, cfg)
|
||||
requireReason(t, err, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
)
|
||||
|
||||
func TestValidateCardTokenizeRequest_ValidTopLevel(t *testing.T) {
|
||||
cfg := testMonetixConfig()
|
||||
req := validCardTokenizeRequest()
|
||||
if _, err := validateCardTokenizeRequest(req, cfg); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCardTokenizeRequest_ValidNestedCard(t *testing.T) {
|
||||
cfg := testMonetixConfig()
|
||||
req := validCardTokenizeRequest()
|
||||
req.Card = &mntxv1.CardDetails{
|
||||
Pan: "4111111111111111",
|
||||
ExpMonth: req.CardExpMonth,
|
||||
ExpYear: req.CardExpYear,
|
||||
CardHolder: req.CardHolder,
|
||||
Cvv: req.CardCvv,
|
||||
}
|
||||
req.CardPan = ""
|
||||
req.CardExpMonth = 0
|
||||
req.CardExpYear = 0
|
||||
req.CardHolder = ""
|
||||
req.CardCvv = ""
|
||||
|
||||
if _, err := validateCardTokenizeRequest(req, cfg); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCardTokenizeRequest_Expired(t *testing.T) {
|
||||
cfg := testMonetixConfig()
|
||||
req := validCardTokenizeRequest()
|
||||
now := time.Now().UTC()
|
||||
req.CardExpMonth = uint32(now.Month())
|
||||
req.CardExpYear = uint32(now.Year() - 1)
|
||||
|
||||
_, err := validateCardTokenizeRequest(req, cfg)
|
||||
requireReason(t, err, "expired_card")
|
||||
}
|
||||
|
||||
func TestValidateCardTokenizeRequest_MissingCvv(t *testing.T) {
|
||||
cfg := testMonetixConfig()
|
||||
req := validCardTokenizeRequest()
|
||||
req.CardCvv = ""
|
||||
|
||||
_, err := validateCardTokenizeRequest(req, cfg)
|
||||
requireReason(t, err, "missing_cvv")
|
||||
}
|
||||
|
||||
func TestValidateCardTokenizeRequest_MissingCardPan(t *testing.T) {
|
||||
cfg := testMonetixConfig()
|
||||
req := validCardTokenizeRequest()
|
||||
req.CardPan = ""
|
||||
|
||||
_, err := validateCardTokenizeRequest(req, cfg)
|
||||
requireReason(t, err, "missing_card_pan")
|
||||
}
|
||||
|
||||
func TestValidateCardTokenizeRequest_AddressRequired(t *testing.T) {
|
||||
cfg := testMonetixConfig()
|
||||
cfg.RequireCustomerAddress = true
|
||||
req := validCardTokenizeRequest()
|
||||
req.CustomerCountry = ""
|
||||
|
||||
_, err := validateCardTokenizeRequest(req, cfg)
|
||||
requireReason(t, err, "missing_customer_country")
|
||||
}
|
||||
@@ -164,3 +164,11 @@ func statusLabel(err error) string {
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCallbackStatus(status string) string {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return strings.ToLower(status)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (s *Service) GetPayout(ctx context.Context, req *mntxv1.GetPayoutRequest) (*mntxv1.GetPayoutResponse, error) {
|
||||
@@ -18,19 +17,14 @@ func (s *Service) GetPayout(ctx context.Context, req *mntxv1.GetPayoutRequest) (
|
||||
|
||||
func (s *Service) handleGetPayout(_ context.Context, req *mntxv1.GetPayoutRequest) gsresponse.Responder[mntxv1.GetPayoutResponse] {
|
||||
ref := strings.TrimSpace(req.GetPayoutRef())
|
||||
log := s.logger.Named("payout")
|
||||
log.Info("Get payout request received", zap.String("payout_ref", ref))
|
||||
if ref == "" {
|
||||
log.Warn("Get payout request missing payout_ref")
|
||||
return gsresponse.InvalidArgument[mntxv1.GetPayoutResponse](s.logger, mservice.MntxGateway, merrors.InvalidArgument("payout_ref is required", "payout_ref"))
|
||||
}
|
||||
|
||||
payout, ok := s.store.Get(ref)
|
||||
if !ok {
|
||||
log.Warn("Payout not found", zap.String("payout_ref", ref))
|
||||
return gsresponse.NotFound[mntxv1.GetPayoutResponse](s.logger, mservice.MntxGateway, merrors.NoData(fmt.Sprintf("payout %s not found", ref)))
|
||||
}
|
||||
|
||||
log.Info("Payout retrieved", zap.String("payout_ref", ref), zap.String("status", payout.GetStatus().String()))
|
||||
return gsresponse.Success(&mntxv1.GetPayoutResponse{Payout: payout})
|
||||
}
|
||||
|
||||
@@ -22,17 +22,8 @@ func (s *Service) SubmitPayout(ctx context.Context, req *mntxv1.SubmitPayoutRequ
|
||||
}
|
||||
|
||||
func (s *Service) handleSubmitPayout(_ context.Context, req *mntxv1.SubmitPayoutRequest) gsresponse.Responder[mntxv1.SubmitPayoutResponse] {
|
||||
log := s.logger.Named("payout")
|
||||
log.Info("Submit payout request received",
|
||||
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||
zap.String("organization_ref", strings.TrimSpace(req.GetOrganizationRef())),
|
||||
zap.String("currency", strings.TrimSpace(req.GetAmount().GetCurrency())),
|
||||
zap.String("amount", strings.TrimSpace(req.GetAmount().GetAmount())),
|
||||
)
|
||||
|
||||
payout, err := s.buildPayout(req)
|
||||
if err != nil {
|
||||
log.Warn("Submit payout validation failed", zap.Error(err))
|
||||
return gsresponse.Auto[mntxv1.SubmitPayoutResponse](s.logger, mservice.MntxGateway, err)
|
||||
}
|
||||
|
||||
@@ -40,7 +31,6 @@ func (s *Service) handleSubmitPayout(_ context.Context, req *mntxv1.SubmitPayout
|
||||
s.emitEvent(payout, nm.NAPending)
|
||||
go s.completePayout(payout, strings.TrimSpace(req.GetSimulatedFailureReason()))
|
||||
|
||||
log.Info("Payout accepted", zap.String("payout_ref", payout.GetPayoutRef()), zap.String("status", payout.GetStatus().String()))
|
||||
return gsresponse.Success(&mntxv1.SubmitPayoutResponse{Payout: payout})
|
||||
}
|
||||
|
||||
@@ -89,7 +79,6 @@ func (s *Service) buildPayout(req *mntxv1.SubmitPayoutRequest) (*mntxv1.Payout,
|
||||
}
|
||||
|
||||
func (s *Service) completePayout(original *mntxv1.Payout, simulatedFailure string) {
|
||||
log := s.logger.Named("payout")
|
||||
outcome := clonePayout(original)
|
||||
if outcome == nil {
|
||||
return
|
||||
@@ -106,7 +95,6 @@ func (s *Service) completePayout(original *mntxv1.Payout, simulatedFailure strin
|
||||
observePayoutError(simulatedFailure, outcome.Amount)
|
||||
s.store.Save(outcome)
|
||||
s.emitEvent(outcome, nm.NAUpdated)
|
||||
log.Info("Payout completed", zap.String("payout_ref", outcome.GetPayoutRef()), zap.String("status", outcome.GetStatus().String()), zap.String("failure_reason", simulatedFailure))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -114,7 +102,6 @@ func (s *Service) completePayout(original *mntxv1.Payout, simulatedFailure strin
|
||||
observePayoutSuccess(outcome.Amount)
|
||||
s.store.Save(outcome)
|
||||
s.emitEvent(outcome, nm.NAUpdated)
|
||||
log.Info("Payout completed", zap.String("payout_ref", outcome.GetPayoutRef()), zap.String("status", outcome.GetStatus().String()))
|
||||
}
|
||||
|
||||
func (s *Service) emitEvent(payout *mntxv1.Payout, action nm.NotificationAction) {
|
||||
@@ -124,18 +111,18 @@ func (s *Service) emitEvent(payout *mntxv1.Payout, action nm.NotificationAction)
|
||||
|
||||
payload, err := protojson.Marshal(&mntxv1.PayoutStatusChangedEvent{Payout: payout})
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to marshal payout event", zapError(err))
|
||||
s.logger.Warn("failed to marshal payout event", zapError(err))
|
||||
return
|
||||
}
|
||||
|
||||
env := messaging.CreateEnvelope(string(mservice.MntxGateway), model.NewNotification(mservice.MntxGateway, action))
|
||||
if _, err := env.Wrap(payload); err != nil {
|
||||
s.logger.Warn("Failed to wrap payout event payload", zapError(err))
|
||||
s.logger.Warn("failed to wrap payout event payload", zapError(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.producer.SendMessage(env); err != nil {
|
||||
s.logger.Warn("Failed to publish payout event", zapError(err))
|
||||
s.logger.Warn("failed to publish payout event", zapError(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -98,19 +97,9 @@ func (s *Service) Register(router routers.GRPC) error {
|
||||
}
|
||||
|
||||
func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) {
|
||||
log := svc.logger.Named("rpc")
|
||||
log.Info("RPC request started", zap.String("method", method))
|
||||
|
||||
start := svc.clock.Now()
|
||||
resp, err := gsresponse.Unary(svc.logger, mservice.MntxGateway, handler)(ctx, req)
|
||||
duration := svc.clock.Now().Sub(start)
|
||||
observeRPC(method, err, duration)
|
||||
|
||||
if err != nil {
|
||||
log.Warn("RPC request failed", zap.String("method", method), zap.Duration("duration", duration), zap.Error(err))
|
||||
} else {
|
||||
log.Info("RPC request completed", zap.String("method", method), zap.Duration("duration", duration))
|
||||
}
|
||||
observeRPC(method, err, svc.clock.Now().Sub(start))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
)
|
||||
|
||||
func requireReason(t *testing.T, err error, reason string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument error, got %v", err)
|
||||
}
|
||||
reasoned, ok := err.(payoutFailure)
|
||||
if !ok {
|
||||
t.Fatalf("expected payout failure reason, got %T", err)
|
||||
}
|
||||
if reasoned.Reason() != reason {
|
||||
t.Fatalf("expected reason %q, got %q", reason, reasoned.Reason())
|
||||
}
|
||||
}
|
||||
|
||||
func testMonetixConfig() monetix.Config {
|
||||
return monetix.Config{
|
||||
AllowedCurrencies: []string{"RUB", "USD"},
|
||||
}
|
||||
}
|
||||
|
||||
func validCardPayoutRequest() *mntxv1.CardPayoutRequest {
|
||||
return &mntxv1.CardPayoutRequest{
|
||||
PayoutId: "payout-1",
|
||||
CustomerId: "cust-1",
|
||||
CustomerFirstName: "Jane",
|
||||
CustomerLastName: "Doe",
|
||||
CustomerIp: "203.0.113.10",
|
||||
AmountMinor: 1500,
|
||||
Currency: "RUB",
|
||||
CardPan: "4111111111111111",
|
||||
CardHolder: "JANE DOE",
|
||||
CardExpMonth: 12,
|
||||
CardExpYear: 2035,
|
||||
}
|
||||
}
|
||||
|
||||
func validCardTokenPayoutRequest() *mntxv1.CardTokenPayoutRequest {
|
||||
return &mntxv1.CardTokenPayoutRequest{
|
||||
PayoutId: "payout-1",
|
||||
CustomerId: "cust-1",
|
||||
CustomerFirstName: "Jane",
|
||||
CustomerLastName: "Doe",
|
||||
CustomerIp: "203.0.113.11",
|
||||
AmountMinor: 2500,
|
||||
Currency: "USD",
|
||||
CardToken: "tok_123",
|
||||
}
|
||||
}
|
||||
|
||||
func validCardTokenizeRequest() *mntxv1.CardTokenizeRequest {
|
||||
month, year := futureExpiry()
|
||||
return &mntxv1.CardTokenizeRequest{
|
||||
RequestId: "req-1",
|
||||
CustomerId: "cust-1",
|
||||
CustomerFirstName: "Jane",
|
||||
CustomerLastName: "Doe",
|
||||
CustomerIp: "203.0.113.12",
|
||||
CardPan: "4111111111111111",
|
||||
CardHolder: "JANE DOE",
|
||||
CardCvv: "123",
|
||||
CardExpMonth: month,
|
||||
CardExpYear: year,
|
||||
}
|
||||
}
|
||||
|
||||
func futureExpiry() (uint32, uint32) {
|
||||
now := time.Now().UTC()
|
||||
return uint32(now.Month()), uint32(now.Year() + 1)
|
||||
}
|
||||
@@ -2,6 +2,10 @@ package monetix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
@@ -41,3 +45,21 @@ func (c *Client) CreateCardTokenPayout(ctx context.Context, req CardTokenPayoutR
|
||||
func (c *Client) CreateCardTokenization(ctx context.Context, req CardTokenizeRequest) (*TokenizationResult, error) {
|
||||
return c.sendTokenization(ctx, req)
|
||||
}
|
||||
|
||||
func signPayload(payload any, secret string) (string, error) {
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
if _, err := h.Write(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// SignPayload exposes signature calculation for callback verification.
|
||||
func SignPayload(payload any, secret string) (string, error) {
|
||||
return signPayload(payload, secret)
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package monetix
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMaskPAN(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{input: "1234", expected: "****"},
|
||||
{input: "1234567890", expected: "12******90"},
|
||||
{input: "1234567890123456", expected: "123456******3456"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
got := MaskPAN(tc.input)
|
||||
if got != tc.expected {
|
||||
t.Fatalf("expected %q, got %q", tc.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ func (c *Client) sendCardPayout(ctx context.Context, req CardPayoutRequest) (*Ca
|
||||
maskedPAN := MaskPAN(req.Card.PAN)
|
||||
return c.send(ctx, &req, "/v2/payment/card/payout",
|
||||
func() {
|
||||
c.logger.Info("Dispatching Monetix card payout",
|
||||
c.logger.Info("dispatching Monetix card payout",
|
||||
zap.String("payout_id", req.General.PaymentID),
|
||||
zap.Int64("amount_minor", req.Payment.Amount),
|
||||
zap.String("currency", req.Payment.Currency),
|
||||
@@ -47,7 +47,7 @@ func (c *Client) sendCardPayout(ctx context.Context, req CardPayoutRequest) (*Ca
|
||||
func (c *Client) sendCardTokenPayout(ctx context.Context, req CardTokenPayoutRequest) (*CardPayoutSendResult, error) {
|
||||
return c.send(ctx, &req, "/v2/payment/card/payout/token",
|
||||
func() {
|
||||
c.logger.Info("Dispatching Monetix card token payout",
|
||||
c.logger.Info("dispatching Monetix card token payout",
|
||||
zap.String("payout_id", req.General.PaymentID),
|
||||
zap.Int64("amount_minor", req.Payment.Amount),
|
||||
zap.String("currency", req.Payment.Currency),
|
||||
@@ -101,28 +101,17 @@ func (c *Client) sendTokenization(ctx context.Context, req CardTokenizeRequest)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
|
||||
c.logger.Info("Dispatching Monetix card tokenization",
|
||||
c.logger.Info("dispatching Monetix card tokenization",
|
||||
zap.String("request_id", req.General.PaymentID),
|
||||
zap.String("masked_pan", MaskPAN(req.Card.PAN)),
|
||||
)
|
||||
|
||||
logRequestDeadline(c.logger, ctx, url)
|
||||
start := time.Now()
|
||||
resp, err := c.client.Do(httpReq)
|
||||
duration := time.Since(start)
|
||||
if err != nil {
|
||||
observeRequest(outcomeNetworkError, duration)
|
||||
fields := []zap.Field{
|
||||
zap.String("url", url),
|
||||
zap.Error(err),
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
fields = append(fields, zap.NamedError("ctx_error", ctxErr))
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
fields = append(fields, zap.Time("deadline", deadline), zap.Duration("time_until_deadline", time.Until(deadline)))
|
||||
}
|
||||
c.logger.Warn("Monetix tokenization request failed", fields...)
|
||||
c.logger.Warn("monetix tokenization request failed", zap.Error(err))
|
||||
return nil, merrors.Internal("monetix tokenization request failed: " + err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -144,7 +133,7 @@ func (c *Client) sendTokenization(ctx context.Context, req CardTokenizeRequest)
|
||||
var apiResp APIResponse
|
||||
if len(body) > 0 {
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
c.logger.Warn("Failed to decode Monetix tokenization response", zap.String("request_id", req.General.PaymentID), zap.Int("status_code", resp.StatusCode), zap.Error(err))
|
||||
c.logger.Warn("failed to decode Monetix tokenization response", zap.String("request_id", req.General.PaymentID), zap.Int("status_code", resp.StatusCode), zap.Error(err))
|
||||
} else {
|
||||
var tokenData struct {
|
||||
Token string `json:"token"`
|
||||
@@ -232,23 +221,11 @@ func (c *Client) send(ctx context.Context, req any, path string, dispatchLog fun
|
||||
dispatchLog()
|
||||
}
|
||||
|
||||
logRequestDeadline(c.logger, ctx, url)
|
||||
start := time.Now()
|
||||
resp, err := c.client.Do(httpReq)
|
||||
duration := time.Since(start)
|
||||
if err != nil {
|
||||
observeRequest(outcomeNetworkError, duration)
|
||||
fields := []zap.Field{
|
||||
zap.String("url", url),
|
||||
zap.Error(err),
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
fields = append(fields, zap.NamedError("ctx_error", ctxErr))
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
fields = append(fields, zap.Time("deadline", deadline), zap.Duration("time_until_deadline", time.Until(deadline)))
|
||||
}
|
||||
c.logger.Warn("Monetix request failed", fields...)
|
||||
return nil, merrors.Internal("monetix request failed: " + err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -268,7 +245,7 @@ func (c *Client) send(ctx context.Context, req any, path string, dispatchLog fun
|
||||
var apiResp APIResponse
|
||||
if len(body) > 0 {
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
c.logger.Warn("Failed to decode Monetix response", zap.Int("status_code", resp.StatusCode), zap.Error(err))
|
||||
c.logger.Warn("failed to decode Monetix response", zap.Int("status_code", resp.StatusCode), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,23 +288,3 @@ func clearSignature(req any) (func(string), error) {
|
||||
return nil, merrors.Internal("unsupported monetix payload type for signing")
|
||||
}
|
||||
}
|
||||
|
||||
func logRequestDeadline(logger *zap.Logger, ctx context.Context, url string) {
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
logger.Info("Monetix request context is nil", zap.String("url", url))
|
||||
return
|
||||
}
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
logger.Info("Monetix request context has no deadline", zap.String("url", url))
|
||||
return
|
||||
}
|
||||
logger.Info("Monetix request context deadline",
|
||||
zap.String("url", url),
|
||||
zap.Time("deadline", deadline),
|
||||
zap.Duration("time_until_deadline", time.Until(deadline)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package monetix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return f(r)
|
||||
}
|
||||
|
||||
func TestSendCardPayout_SignsPayload(t *testing.T) {
|
||||
secret := "secret"
|
||||
var captured CardPayoutRequest
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.URL.Path != "/v2/payment/card/payout" {
|
||||
t.Fatalf("expected payout path, got %q", r.URL.Path)
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if err := json.Unmarshal(body, &captured); err != nil {
|
||||
t.Fatalf("failed to decode request: %v", err)
|
||||
}
|
||||
resp := APIResponse{}
|
||||
resp.Operation.RequestID = "req-1"
|
||||
payload, _ := json.Marshal(resp)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(payload)),
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
BaseURL: "https://monetix.test",
|
||||
SecretKey: secret,
|
||||
}
|
||||
client := NewClient(cfg, httpClient, zap.NewNop())
|
||||
|
||||
req := CardPayoutRequest{
|
||||
General: General{ProjectID: 1, PaymentID: "payout-1"},
|
||||
Customer: Customer{
|
||||
ID: "cust-1",
|
||||
FirstName: "Jane",
|
||||
LastName: "Doe",
|
||||
IP: "203.0.113.10",
|
||||
},
|
||||
Payment: Payment{Amount: 1000, Currency: "RUB"},
|
||||
Card: Card{PAN: "4111111111111111", Year: 2030, Month: 12, CardHolder: "JANE DOE"},
|
||||
}
|
||||
|
||||
result, err := client.CreateCardPayout(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if !result.Accepted {
|
||||
t.Fatalf("expected accepted response")
|
||||
}
|
||||
if captured.General.Signature == "" {
|
||||
t.Fatalf("expected signature in request")
|
||||
}
|
||||
|
||||
signed := captured
|
||||
signed.General.Signature = ""
|
||||
expectedSig, err := SignPayload(signed, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compute signature: %v", err)
|
||||
}
|
||||
if captured.General.Signature != expectedSig {
|
||||
t.Fatalf("expected signature %q, got %q", expectedSig, captured.General.Signature)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendCardPayout_HTTPError(t *testing.T) {
|
||||
httpClient := &http.Client{
|
||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||
body := `{"code":"E100","message":"denied"}`
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
BaseURL: "https://monetix.test",
|
||||
SecretKey: "secret",
|
||||
}
|
||||
client := NewClient(cfg, httpClient, zap.NewNop())
|
||||
|
||||
req := CardPayoutRequest{
|
||||
General: General{ProjectID: 1, PaymentID: "payout-1"},
|
||||
Customer: Customer{
|
||||
ID: "cust-1",
|
||||
FirstName: "Jane",
|
||||
LastName: "Doe",
|
||||
IP: "203.0.113.10",
|
||||
},
|
||||
Payment: Payment{Amount: 1000, Currency: "RUB"},
|
||||
Card: Card{PAN: "4111111111111111", Year: 2030, Month: 12, CardHolder: "JANE DOE"},
|
||||
}
|
||||
|
||||
result, err := client.CreateCardPayout(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if result.Accepted {
|
||||
t.Fatalf("expected rejected response")
|
||||
}
|
||||
if result.ErrorCode != "E100" {
|
||||
t.Fatalf("expected error code E100, got %q", result.ErrorCode)
|
||||
}
|
||||
if result.ErrorMessage != "denied" {
|
||||
t.Fatalf("expected error message denied, got %q", result.ErrorMessage)
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package monetix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func signPayload(payload any, secret string) (string, error) {
|
||||
canonical, err := signaturePayloadString(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mac := hmac.New(sha512.New, []byte(secret))
|
||||
if _, err := mac.Write([]byte(canonical)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// SignPayload exposes signature calculation for callback verification.
|
||||
func SignPayload(payload any, secret string) (string, error) {
|
||||
return signPayload(payload, secret)
|
||||
}
|
||||
|
||||
func signaturePayloadString(payload any) (string, error) {
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var root any
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(&root); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := make([]string, 0)
|
||||
collectSignatureLines(nil, root, &lines)
|
||||
sort.Strings(lines)
|
||||
|
||||
return strings.Join(lines, ";"), nil
|
||||
}
|
||||
|
||||
func collectSignatureLines(path []string, value any, lines *[]string) {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
for key, child := range v {
|
||||
if strings.EqualFold(key, "signature") {
|
||||
continue
|
||||
}
|
||||
collectSignatureLines(append(path, key), child, lines)
|
||||
}
|
||||
case []any:
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
for idx, child := range v {
|
||||
collectSignatureLines(append(path, strconv.Itoa(idx)), child, lines)
|
||||
}
|
||||
default:
|
||||
line := formatSignatureLine(path, v)
|
||||
if line != "" {
|
||||
*lines = append(*lines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatSignatureLine(path []string, value any) string {
|
||||
if len(path) == 0 {
|
||||
return ""
|
||||
}
|
||||
val := signatureValueString(value)
|
||||
segments := append(append([]string{}, path...), val)
|
||||
return strings.Join(segments, ":")
|
||||
}
|
||||
|
||||
func signatureValueString(value any) string {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case string:
|
||||
return v
|
||||
case json.Number:
|
||||
return v.String()
|
||||
case bool:
|
||||
if v {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(v), 'f', -1, 32)
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case int8, int16, int32, int64:
|
||||
return fmt.Sprint(v)
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return fmt.Sprint(v)
|
||||
default:
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package monetix
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSignaturePayloadString_Example(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"general": map[string]any{
|
||||
"project_id": 3254,
|
||||
"payment_id": "id_38202316",
|
||||
"signature": "<ignored>",
|
||||
},
|
||||
"customer": map[string]any{
|
||||
"id": "585741",
|
||||
"email": "johndoe@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"address": "Downing str., 23",
|
||||
"identify": map[string]any{
|
||||
"doc_number": "54122312544",
|
||||
},
|
||||
"ip_address": "198.51.100.47",
|
||||
},
|
||||
"payment": map[string]any{
|
||||
"amount": 10800,
|
||||
"currency": "USD",
|
||||
"description": "Computer keyboards",
|
||||
},
|
||||
"receipt_data": map[string]any{
|
||||
"positions": []any{
|
||||
map[string]any{
|
||||
"quantity": "10",
|
||||
"amount": "108",
|
||||
"description": "Computer keyboard",
|
||||
},
|
||||
},
|
||||
},
|
||||
"return_url": map[string]any{
|
||||
"success": "https://paymentpage.example.com/complete-redirect?id=success",
|
||||
"decline": "https://paymentpage.example.com/complete-redirect?id=decline",
|
||||
},
|
||||
}
|
||||
|
||||
got, err := signaturePayloadString(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build signature string: %v", err)
|
||||
}
|
||||
|
||||
expected := "customer:address:Downing str., 23;customer:email:johndoe@example.com;customer:first_name:John;customer:id:585741;customer:identify:doc_number:54122312544;customer:ip_address:198.51.100.47;customer:last_name:Doe;general:payment_id:id_38202316;general:project_id:3254;payment:amount:10800;payment:currency:USD;payment:description:Computer keyboards;receipt_data:positions:0:amount:108;receipt_data:positions:0:description:Computer keyboard;receipt_data:positions:0:quantity:10;return_url:decline:https://paymentpage.example.com/complete-redirect?id=decline;return_url:success:https://paymentpage.example.com/complete-redirect?id=success"
|
||||
if got != expected {
|
||||
t.Fatalf("unexpected signature string\nexpected: %s\ngot: %s", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignPayload_Example(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"general": map[string]any{
|
||||
"project_id": 3254,
|
||||
"payment_id": "id_38202316",
|
||||
"signature": "<ignored>",
|
||||
},
|
||||
"customer": map[string]any{
|
||||
"id": "585741",
|
||||
"email": "johndoe@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"address": "Downing str., 23",
|
||||
"identify": map[string]any{
|
||||
"doc_number": "54122312544",
|
||||
},
|
||||
"ip_address": "198.51.100.47",
|
||||
},
|
||||
"payment": map[string]any{
|
||||
"amount": 10800,
|
||||
"currency": "USD",
|
||||
"description": "Computer keyboards",
|
||||
},
|
||||
"receipt_data": map[string]any{
|
||||
"positions": []any{
|
||||
map[string]any{
|
||||
"quantity": "10",
|
||||
"amount": "108",
|
||||
"description": "Computer keyboard",
|
||||
},
|
||||
},
|
||||
},
|
||||
"return_url": map[string]any{
|
||||
"success": "https://paymentpage.example.com/complete-redirect?id=success",
|
||||
"decline": "https://paymentpage.example.com/complete-redirect?id=decline",
|
||||
},
|
||||
}
|
||||
|
||||
got, err := SignPayload(payload, "secret")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign payload: %v", err)
|
||||
}
|
||||
expected := "lagSnuspAn+F6XkmQISqwtBg0PsiTy62fF9x33TM+278mnufIDZyi1yP0BQALuCxyikkIxIMbodBn2F8hMdRwA=="
|
||||
if got != expected {
|
||||
t.Fatalf("unexpected signature\nexpected: %s\ngot: %s", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignaturePayloadString_BooleansAndArrays(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"flag": true,
|
||||
"false_flag": false,
|
||||
"empty": "",
|
||||
"zero": 0,
|
||||
"nested": map[string]any{
|
||||
"list": []any{},
|
||||
"items": []any{"alpha", "beta"},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := signaturePayloadString(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build signature string: %v", err)
|
||||
}
|
||||
|
||||
expected := "empty:;false_flag:0;flag:1;nested:items:0:alpha;nested:items:1:beta;zero:0"
|
||||
if got != expected {
|
||||
t.Fatalf("unexpected signature string\nexpected: %s\ngot: %s", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignPayload_EthEstimateGasExample(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "eth_estimateGas",
|
||||
"params": []any{
|
||||
map[string]any{
|
||||
"from": "0xfa89b4d534bdeb2713d4ffd893e79d6535fb58f8",
|
||||
"to": "0x44162e39eefd9296231e772663a92e72958e182f",
|
||||
"gasPrice": "0x64",
|
||||
"data": "0xa9059cbb00000000000000000000000044162e39eefd9296231e772663a92e72958e182f00000000000000000000000000000000000000000000000000000000000f4240",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := SignPayload(payload, "1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign payload: %v", err)
|
||||
}
|
||||
expected := "C4WbSvXKSMyX8yLamQcUe/Nzr6nSt9m3HYn4jHSyA7yi/FaTiqk0r8BlfIzfxSCoDaRgrSd82ihgZW+DxELhdQ=="
|
||||
if got != expected {
|
||||
t.Fatalf("unexpected signature\nexpected: %s\ngot: %s", expected, got)
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ gateway:
|
||||
mntx:
|
||||
address: "sendico_mntx_gateway:50075"
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 15
|
||||
call_timeout_seconds: 3
|
||||
insecure: true
|
||||
|
||||
oracle:
|
||||
|
||||
@@ -273,7 +273,6 @@ func (i *Imp) initMntxClient(cfg clientConfig) mntxclient.Client {
|
||||
Address: addr,
|
||||
DialTimeout: cfg.dialTimeout(),
|
||||
CallTimeout: cfg.callTimeout(),
|
||||
Logger: i.logger.Named("client.mntx"),
|
||||
})
|
||||
if err != nil {
|
||||
i.logger.Warn("Failed to connect to mntx gateway service", zap.String("address", addr), zap.Error(err))
|
||||
|
||||
@@ -313,47 +313,6 @@ func (s *Service) submitCardPayout(ctx context.Context, payment *model.Payment)
|
||||
currency := strings.TrimSpace(amount.GetCurrency())
|
||||
holder := strings.TrimSpace(card.Cardholder)
|
||||
meta := cloneMetadata(payment.Metadata)
|
||||
customer := intent.Customer
|
||||
customerID := ""
|
||||
customerFirstName := ""
|
||||
customerMiddleName := ""
|
||||
customerLastName := ""
|
||||
customerIP := ""
|
||||
customerZip := ""
|
||||
customerCountry := ""
|
||||
customerState := ""
|
||||
customerCity := ""
|
||||
customerAddress := ""
|
||||
if customer != nil {
|
||||
customerID = strings.TrimSpace(customer.ID)
|
||||
customerFirstName = strings.TrimSpace(customer.FirstName)
|
||||
customerMiddleName = strings.TrimSpace(customer.MiddleName)
|
||||
customerLastName = strings.TrimSpace(customer.LastName)
|
||||
customerIP = strings.TrimSpace(customer.IP)
|
||||
customerZip = strings.TrimSpace(customer.Zip)
|
||||
customerCountry = strings.TrimSpace(customer.Country)
|
||||
customerState = strings.TrimSpace(customer.State)
|
||||
customerCity = strings.TrimSpace(customer.City)
|
||||
customerAddress = strings.TrimSpace(customer.Address)
|
||||
}
|
||||
if customerFirstName == "" {
|
||||
customerFirstName = strings.TrimSpace(card.Cardholder)
|
||||
}
|
||||
if customerLastName == "" {
|
||||
customerLastName = strings.TrimSpace(card.CardholderSurname)
|
||||
}
|
||||
if customerID == "" {
|
||||
return merrors.InvalidArgument("card payout: customer id is required")
|
||||
}
|
||||
if customerFirstName == "" {
|
||||
return merrors.InvalidArgument("card payout: customer first name is required")
|
||||
}
|
||||
if customerLastName == "" {
|
||||
return merrors.InvalidArgument("card payout: customer last name is required")
|
||||
}
|
||||
if customerIP == "" {
|
||||
return merrors.InvalidArgument("card payout: customer ip is required")
|
||||
}
|
||||
|
||||
var (
|
||||
state *mntxv1.CardPayoutState
|
||||
@@ -361,23 +320,13 @@ func (s *Service) submitCardPayout(ctx context.Context, payment *model.Payment)
|
||||
|
||||
if token := strings.TrimSpace(card.Token); token != "" {
|
||||
req := &mntxv1.CardTokenPayoutRequest{
|
||||
PayoutId: payoutID,
|
||||
CustomerId: customerID,
|
||||
CustomerFirstName: customerFirstName,
|
||||
CustomerMiddleName: customerMiddleName,
|
||||
CustomerLastName: customerLastName,
|
||||
CustomerIp: customerIP,
|
||||
CustomerZip: customerZip,
|
||||
CustomerCountry: customerCountry,
|
||||
CustomerState: customerState,
|
||||
CustomerCity: customerCity,
|
||||
CustomerAddress: customerAddress,
|
||||
AmountMinor: minor,
|
||||
Currency: currency,
|
||||
CardToken: token,
|
||||
CardHolder: holder,
|
||||
MaskedPan: strings.TrimSpace(card.MaskedPan),
|
||||
Metadata: meta,
|
||||
PayoutId: payoutID,
|
||||
AmountMinor: minor,
|
||||
Currency: currency,
|
||||
CardToken: token,
|
||||
CardHolder: holder,
|
||||
MaskedPan: strings.TrimSpace(card.MaskedPan),
|
||||
Metadata: meta,
|
||||
}
|
||||
resp, err := s.deps.mntx.client.CreateCardTokenPayout(ctx, req)
|
||||
if err != nil {
|
||||
@@ -387,24 +336,14 @@ func (s *Service) submitCardPayout(ctx context.Context, payment *model.Payment)
|
||||
state = resp.GetPayout()
|
||||
} else if pan := strings.TrimSpace(card.Pan); pan != "" {
|
||||
req := &mntxv1.CardPayoutRequest{
|
||||
PayoutId: payoutID,
|
||||
CustomerId: customerID,
|
||||
CustomerFirstName: customerFirstName,
|
||||
CustomerMiddleName: customerMiddleName,
|
||||
CustomerLastName: customerLastName,
|
||||
CustomerIp: customerIP,
|
||||
CustomerZip: customerZip,
|
||||
CustomerCountry: customerCountry,
|
||||
CustomerState: customerState,
|
||||
CustomerCity: customerCity,
|
||||
CustomerAddress: customerAddress,
|
||||
AmountMinor: minor,
|
||||
Currency: currency,
|
||||
CardPan: pan,
|
||||
CardExpYear: card.ExpYear,
|
||||
CardExpMonth: card.ExpMonth,
|
||||
CardHolder: holder,
|
||||
Metadata: meta,
|
||||
PayoutId: payoutID,
|
||||
AmountMinor: minor,
|
||||
Currency: currency,
|
||||
CardPan: pan,
|
||||
CardExpYear: card.ExpYear,
|
||||
CardExpMonth: card.ExpMonth,
|
||||
CardHolder: holder,
|
||||
Metadata: meta,
|
||||
}
|
||||
resp, err := s.deps.mntx.client.CreateCardPayout(ctx, req)
|
||||
if err != nil {
|
||||
|
||||
@@ -266,12 +266,6 @@ func TestSubmitCardPayout_UsesSettlementAmountAndTransfersFee(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Amount: &moneyv1.Money{Currency: "USDT", Amount: "5"},
|
||||
Customer: &model.Customer{
|
||||
ID: "recipient-1",
|
||||
FirstName: "Stephan",
|
||||
LastName: "Tester",
|
||||
IP: "198.51.100.10",
|
||||
},
|
||||
},
|
||||
LastQuote: &model.PaymentQuoteSnapshot{
|
||||
ExpectedSettlementAmount: &moneyv1.Money{Currency: "RUB", Amount: "392.30"},
|
||||
|
||||
@@ -26,7 +26,6 @@ func intentFromProto(src *orchestratorv1.PaymentIntent) model.PaymentIntent {
|
||||
FeePolicy: src.GetFeePolicy(),
|
||||
SettlementMode: src.GetSettlementMode(),
|
||||
Attributes: cloneMetadata(src.GetAttributes()),
|
||||
Customer: customerFromProto(src.GetCustomer()),
|
||||
}
|
||||
if src.GetFx() != nil {
|
||||
intent.FX = fxIntentFromProto(src.GetFx())
|
||||
@@ -70,14 +69,13 @@ func endpointFromProto(src *orchestratorv1.PaymentEndpoint) model.PaymentEndpoin
|
||||
if card := src.GetCard(); card != nil {
|
||||
result.Type = model.EndpointTypeCard
|
||||
result.Card = &model.CardEndpoint{
|
||||
Pan: strings.TrimSpace(card.GetPan()),
|
||||
Token: strings.TrimSpace(card.GetToken()),
|
||||
Cardholder: strings.TrimSpace(card.GetCardholderName()),
|
||||
CardholderSurname: strings.TrimSpace(card.GetCardholderSurname()),
|
||||
ExpMonth: card.GetExpMonth(),
|
||||
ExpYear: card.GetExpYear(),
|
||||
Country: strings.TrimSpace(card.GetCountry()),
|
||||
MaskedPan: strings.TrimSpace(card.GetMaskedPan()),
|
||||
Pan: strings.TrimSpace(card.GetPan()),
|
||||
Token: strings.TrimSpace(card.GetToken()),
|
||||
Cardholder: strings.TrimSpace(card.GetCardholderName()),
|
||||
ExpMonth: card.GetExpMonth(),
|
||||
ExpYear: card.GetExpYear(),
|
||||
Country: strings.TrimSpace(card.GetCountry()),
|
||||
MaskedPan: strings.TrimSpace(card.GetMaskedPan()),
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -163,7 +161,6 @@ func protoIntentFromModel(src model.PaymentIntent) *orchestratorv1.PaymentIntent
|
||||
FeePolicy: src.FeePolicy,
|
||||
SettlementMode: src.SettlementMode,
|
||||
Attributes: cloneMetadata(src.Attributes),
|
||||
Customer: protoCustomerFromModel(src.Customer),
|
||||
}
|
||||
if src.FX != nil {
|
||||
intent.Fx = protoFXIntentFromModel(src.FX)
|
||||
@@ -171,42 +168,6 @@ func protoIntentFromModel(src model.PaymentIntent) *orchestratorv1.PaymentIntent
|
||||
return intent
|
||||
}
|
||||
|
||||
func customerFromProto(src *orchestratorv1.Customer) *model.Customer {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
return &model.Customer{
|
||||
ID: strings.TrimSpace(src.GetId()),
|
||||
FirstName: strings.TrimSpace(src.GetFirstName()),
|
||||
MiddleName: strings.TrimSpace(src.GetMiddleName()),
|
||||
LastName: strings.TrimSpace(src.GetLastName()),
|
||||
IP: strings.TrimSpace(src.GetIp()),
|
||||
Zip: strings.TrimSpace(src.GetZip()),
|
||||
Country: strings.TrimSpace(src.GetCountry()),
|
||||
State: strings.TrimSpace(src.GetState()),
|
||||
City: strings.TrimSpace(src.GetCity()),
|
||||
Address: strings.TrimSpace(src.GetAddress()),
|
||||
}
|
||||
}
|
||||
|
||||
func protoCustomerFromModel(src *model.Customer) *orchestratorv1.Customer {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
return &orchestratorv1.Customer{
|
||||
Id: strings.TrimSpace(src.ID),
|
||||
FirstName: strings.TrimSpace(src.FirstName),
|
||||
MiddleName: strings.TrimSpace(src.MiddleName),
|
||||
LastName: strings.TrimSpace(src.LastName),
|
||||
Ip: strings.TrimSpace(src.IP),
|
||||
Zip: strings.TrimSpace(src.Zip),
|
||||
Country: strings.TrimSpace(src.Country),
|
||||
State: strings.TrimSpace(src.State),
|
||||
City: strings.TrimSpace(src.City),
|
||||
Address: strings.TrimSpace(src.Address),
|
||||
}
|
||||
}
|
||||
|
||||
func protoEndpointFromModel(src model.PaymentEndpoint) *orchestratorv1.PaymentEndpoint {
|
||||
endpoint := &orchestratorv1.PaymentEndpoint{
|
||||
Metadata: cloneMetadata(src.Metadata),
|
||||
@@ -243,12 +204,11 @@ func protoEndpointFromModel(src model.PaymentEndpoint) *orchestratorv1.PaymentEn
|
||||
case model.EndpointTypeCard:
|
||||
if src.Card != nil {
|
||||
card := &orchestratorv1.CardEndpoint{
|
||||
CardholderName: src.Card.Cardholder,
|
||||
CardholderSurname: src.Card.CardholderSurname,
|
||||
ExpMonth: src.Card.ExpMonth,
|
||||
ExpYear: src.Card.ExpYear,
|
||||
Country: src.Card.Country,
|
||||
MaskedPan: src.Card.MaskedPan,
|
||||
CardholderName: src.Card.Cardholder,
|
||||
ExpMonth: src.Card.ExpMonth,
|
||||
ExpYear: src.Card.ExpYear,
|
||||
Country: src.Card.Country,
|
||||
MaskedPan: src.Card.MaskedPan,
|
||||
}
|
||||
if pan := strings.TrimSpace(src.Card.Pan); pan != "" {
|
||||
card.Card = &orchestratorv1.CardEndpoint_Pan{Pan: pan}
|
||||
|
||||
@@ -11,13 +11,12 @@ func TestEndpointFromProtoCard(t *testing.T) {
|
||||
protoEndpoint := &orchestratorv1.PaymentEndpoint{
|
||||
Endpoint: &orchestratorv1.PaymentEndpoint_Card{
|
||||
Card: &orchestratorv1.CardEndpoint{
|
||||
Card: &orchestratorv1.CardEndpoint_Pan{Pan: " 411111 "},
|
||||
CardholderName: " Jane ",
|
||||
CardholderSurname: " Doe ",
|
||||
ExpMonth: 12,
|
||||
ExpYear: 2030,
|
||||
Country: " US ",
|
||||
MaskedPan: " ****1111 ",
|
||||
Card: &orchestratorv1.CardEndpoint_Pan{Pan: " 411111 "},
|
||||
CardholderName: " Jane Doe ",
|
||||
ExpMonth: 12,
|
||||
ExpYear: 2030,
|
||||
Country: " US ",
|
||||
MaskedPan: " ****1111 ",
|
||||
},
|
||||
},
|
||||
Metadata: map[string]string{"k": "v"},
|
||||
@@ -30,7 +29,7 @@ func TestEndpointFromProtoCard(t *testing.T) {
|
||||
if modelEndpoint.Card == nil {
|
||||
t.Fatalf("card payload missing")
|
||||
}
|
||||
if modelEndpoint.Card.Pan != "411111" || modelEndpoint.Card.Cardholder != "Jane" || modelEndpoint.Card.CardholderSurname != "Doe" || modelEndpoint.Card.Country != "US" || modelEndpoint.Card.MaskedPan != "****1111" {
|
||||
if modelEndpoint.Card.Pan != "411111" || modelEndpoint.Card.Cardholder != "Jane Doe" || modelEndpoint.Card.Country != "US" || modelEndpoint.Card.MaskedPan != "****1111" {
|
||||
t.Fatalf("card payload not trimmed as expected: %#v", modelEndpoint.Card)
|
||||
}
|
||||
if modelEndpoint.Metadata["k"] != "v" {
|
||||
@@ -42,13 +41,12 @@ func TestProtoEndpointFromModelCard(t *testing.T) {
|
||||
modelEndpoint := model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeCard,
|
||||
Card: &model.CardEndpoint{
|
||||
Token: "tok_123",
|
||||
Cardholder: "Jane",
|
||||
CardholderSurname: "Doe",
|
||||
ExpMonth: 1,
|
||||
ExpYear: 2028,
|
||||
Country: "GB",
|
||||
MaskedPan: "****1234",
|
||||
Token: "tok_123",
|
||||
Cardholder: "Jane",
|
||||
ExpMonth: 1,
|
||||
ExpYear: 2028,
|
||||
Country: "GB",
|
||||
MaskedPan: "****1234",
|
||||
},
|
||||
Metadata: map[string]string{"k": "v"},
|
||||
}
|
||||
@@ -62,7 +60,7 @@ func TestProtoEndpointFromModelCard(t *testing.T) {
|
||||
if !ok || token.Token != "tok_123" {
|
||||
t.Fatalf("expected token payload, got %T %#v", card.Card, card.Card)
|
||||
}
|
||||
if card.GetCardholderName() != "Jane" || card.GetCardholderSurname() != "Doe" || card.GetCountry() != "GB" || card.GetMaskedPan() != "****1234" {
|
||||
if card.GetCardholderName() != "Jane" || card.GetCountry() != "GB" || card.GetMaskedPan() != "****1234" {
|
||||
t.Fatalf("card details mismatch: %#v", card)
|
||||
}
|
||||
if protoEndpoint.GetMetadata()["k"] != "v" {
|
||||
|
||||
@@ -82,14 +82,13 @@ type ExternalChainEndpoint struct {
|
||||
|
||||
// CardEndpoint describes a card payout destination.
|
||||
type CardEndpoint struct {
|
||||
Pan string `bson:"pan,omitempty" json:"pan,omitempty"`
|
||||
Token string `bson:"token,omitempty" json:"token,omitempty"`
|
||||
Cardholder string `bson:"cardholder,omitempty" json:"cardholder,omitempty"`
|
||||
CardholderSurname string `bson:"cardholderSurname,omitempty" json:"cardholderSurname,omitempty"`
|
||||
ExpMonth uint32 `bson:"expMonth,omitempty" json:"expMonth,omitempty"`
|
||||
ExpYear uint32 `bson:"expYear,omitempty" json:"expYear,omitempty"`
|
||||
Country string `bson:"country,omitempty" json:"country,omitempty"`
|
||||
MaskedPan string `bson:"maskedPan,omitempty" json:"maskedPan,omitempty"`
|
||||
Pan string `bson:"pan,omitempty" json:"pan,omitempty"`
|
||||
Token string `bson:"token,omitempty" json:"token,omitempty"`
|
||||
Cardholder string `bson:"cardholder,omitempty" json:"cardholder,omitempty"`
|
||||
ExpMonth uint32 `bson:"expMonth,omitempty" json:"expMonth,omitempty"`
|
||||
ExpYear uint32 `bson:"expYear,omitempty" json:"expYear,omitempty"`
|
||||
Country string `bson:"country,omitempty" json:"country,omitempty"`
|
||||
MaskedPan string `bson:"maskedPan,omitempty" json:"maskedPan,omitempty"`
|
||||
}
|
||||
|
||||
// CardPayout stores gateway payout tracking info.
|
||||
@@ -135,21 +134,6 @@ type PaymentIntent struct {
|
||||
FeePolicy *feesv1.PolicyOverrides `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"`
|
||||
SettlementMode orchestratorv1.SettlementMode `bson:"settlementMode,omitempty" json:"settlementMode,omitempty"`
|
||||
Attributes map[string]string `bson:"attributes,omitempty" json:"attributes,omitempty"`
|
||||
Customer *Customer `bson:"customer,omitempty" json:"customer,omitempty"`
|
||||
}
|
||||
|
||||
// Customer captures payer/recipient identity details for downstream processing.
|
||||
type Customer struct {
|
||||
ID string `bson:"id,omitempty" json:"id,omitempty"`
|
||||
FirstName string `bson:"firstName,omitempty" json:"firstName,omitempty"`
|
||||
MiddleName string `bson:"middleName,omitempty" json:"middleName,omitempty"`
|
||||
LastName string `bson:"lastName,omitempty" json:"lastName,omitempty"`
|
||||
IP string `bson:"ip,omitempty" json:"ip,omitempty"`
|
||||
Zip string `bson:"zip,omitempty" json:"zip,omitempty"`
|
||||
Country string `bson:"country,omitempty" json:"country,omitempty"`
|
||||
State string `bson:"state,omitempty" json:"state,omitempty"`
|
||||
City string `bson:"city,omitempty" json:"city,omitempty"`
|
||||
Address string `bson:"address,omitempty" json:"address,omitempty"`
|
||||
}
|
||||
|
||||
// PaymentQuoteSnapshot stores the latest quote info.
|
||||
@@ -188,8 +172,8 @@ type ExecutionStep struct {
|
||||
|
||||
// ExecutionPlan captures the ordered list of steps to execute a payment.
|
||||
type ExecutionPlan struct {
|
||||
Steps []*ExecutionStep `bson:"steps,omitempty" json:"steps,omitempty"`
|
||||
TotalNetworkFee *moneyv1.Money `bson:"totalNetworkFee,omitempty" json:"totalNetworkFee,omitempty"`
|
||||
Steps []*ExecutionStep `bson:"steps,omitempty" json:"steps,omitempty"`
|
||||
TotalNetworkFee *moneyv1.Money `bson:"totalNetworkFee,omitempty" json:"totalNetworkFee,omitempty"`
|
||||
}
|
||||
|
||||
// Payment persists orchestrated payment lifecycle.
|
||||
@@ -247,18 +231,6 @@ func (p *Payment) Normalize() {
|
||||
p.Intent.Attributes[k] = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
if p.Intent.Customer != nil {
|
||||
p.Intent.Customer.ID = strings.TrimSpace(p.Intent.Customer.ID)
|
||||
p.Intent.Customer.FirstName = strings.TrimSpace(p.Intent.Customer.FirstName)
|
||||
p.Intent.Customer.MiddleName = strings.TrimSpace(p.Intent.Customer.MiddleName)
|
||||
p.Intent.Customer.LastName = strings.TrimSpace(p.Intent.Customer.LastName)
|
||||
p.Intent.Customer.IP = strings.TrimSpace(p.Intent.Customer.IP)
|
||||
p.Intent.Customer.Zip = strings.TrimSpace(p.Intent.Customer.Zip)
|
||||
p.Intent.Customer.Country = strings.TrimSpace(p.Intent.Customer.Country)
|
||||
p.Intent.Customer.State = strings.TrimSpace(p.Intent.Customer.State)
|
||||
p.Intent.Customer.City = strings.TrimSpace(p.Intent.Customer.City)
|
||||
p.Intent.Customer.Address = strings.TrimSpace(p.Intent.Customer.Address)
|
||||
}
|
||||
if p.Execution != nil {
|
||||
p.Execution.DebitEntryRef = strings.TrimSpace(p.Execution.DebitEntryRef)
|
||||
p.Execution.CreditEntryRef = strings.TrimSpace(p.Execution.CreditEntryRef)
|
||||
@@ -321,7 +293,6 @@ func normalizeEndpoint(ep *PaymentEndpoint) {
|
||||
ep.Card.Pan = strings.TrimSpace(ep.Card.Pan)
|
||||
ep.Card.Token = strings.TrimSpace(ep.Card.Token)
|
||||
ep.Card.Cardholder = strings.TrimSpace(ep.Card.Cardholder)
|
||||
ep.Card.CardholderSurname = strings.TrimSpace(ep.Card.CardholderSurname)
|
||||
ep.Card.Country = strings.TrimSpace(ep.Card.Country)
|
||||
ep.Card.MaskedPan = strings.TrimSpace(ep.Card.MaskedPan)
|
||||
}
|
||||
|
||||
@@ -112,20 +112,6 @@ message PaymentIntent {
|
||||
fees.v1.PolicyOverrides fee_policy = 7;
|
||||
map<string, string> attributes = 8;
|
||||
SettlementMode settlement_mode = 9;
|
||||
Customer customer = 10;
|
||||
}
|
||||
|
||||
message Customer {
|
||||
string id = 1;
|
||||
string first_name = 2;
|
||||
string middle_name = 3;
|
||||
string last_name = 4;
|
||||
string ip = 5;
|
||||
string zip = 6;
|
||||
string country = 7;
|
||||
string state = 8;
|
||||
string city = 9;
|
||||
string address = 10;
|
||||
}
|
||||
|
||||
message PaymentQuote {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package srequest
|
||||
|
||||
// Customer captures payer/recipient identity details for downstream processing.
|
||||
type Customer struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
MiddleName string `json:"middle_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
Zip string `json:"zip,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
@@ -13,7 +13,6 @@ type PaymentIntent struct {
|
||||
FX *FXIntent `json:"fx,omitempty"`
|
||||
SettlementMode SettlementMode `json:"settlement_mode,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
Customer *Customer `json:"customer,omitempty"`
|
||||
}
|
||||
|
||||
type AssetResolverStub struct{}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||
)
|
||||
@@ -76,8 +75,7 @@ type paymentQuotesResponse struct {
|
||||
|
||||
type paymentsResponse struct {
|
||||
authResponse `json:",inline"`
|
||||
Payments []Payment `json:"payments"`
|
||||
Page *paginationv1.CursorPageResponse `json:"page,omitempty"`
|
||||
Payments []Payment `json:"payments"`
|
||||
}
|
||||
|
||||
type paymentResponse struct {
|
||||
@@ -109,15 +107,6 @@ func PaymentsResponse(logger mlogger.Logger, payments []*orchestratorv1.Payment,
|
||||
})
|
||||
}
|
||||
|
||||
// PaymentsList wraps a list of payments with refreshed access token and pagination data.
|
||||
func PaymentsListResponse(logger mlogger.Logger, resp *orchestratorv1.ListPaymentsResponse, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentsResponse{
|
||||
Payments: toPayments(resp.GetPayments()),
|
||||
Page: resp.GetPage(),
|
||||
authResponse: authResponse{AccessToken: *token},
|
||||
})
|
||||
}
|
||||
|
||||
// Payment wraps a payment with refreshed access token.
|
||||
func PaymentResponse(logger mlogger.Logger, payment *orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentResponse{
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package paymentapiimp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
)
|
||||
|
||||
func applyCustomerIP(intent *srequest.PaymentIntent, remoteAddr string) {
|
||||
if intent == nil {
|
||||
return
|
||||
}
|
||||
ip := strings.TrimSpace(remoteAddr)
|
||||
if ip == "" {
|
||||
return
|
||||
}
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil && host != "" {
|
||||
ip = host
|
||||
}
|
||||
if intent.Customer == nil {
|
||||
intent.Customer = &srequest.Customer{}
|
||||
}
|
||||
intent.Customer.IP = strings.TrimSpace(ip)
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package paymentapiimp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const maxInt32 = int64(1<<31 - 1)
|
||||
|
||||
func (a *PaymentAPI) listPayments(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
|
||||
orgRef, err := a.oph.GetRef(r)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to parse organization reference for payments list", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
if !allowed {
|
||||
a.logger.Debug("Access denied when listing payments", mutil.PLog(a.oph, r))
|
||||
return response.AccessDenied(a.logger, a.Name(), "payments read permission denied")
|
||||
}
|
||||
|
||||
req := &orchestratorv1.ListPaymentsRequest{
|
||||
Meta: &orchestratorv1.RequestMeta{
|
||||
OrganizationRef: orgRef.Hex(),
|
||||
},
|
||||
}
|
||||
|
||||
if page, err := listPaymentsPage(r); err != nil {
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
} else if page != nil {
|
||||
req.Page = page
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
if sourceRef := strings.TrimSpace(query.Get("source_ref")); sourceRef != "" {
|
||||
req.SourceRef = sourceRef
|
||||
}
|
||||
if destinationRef := strings.TrimSpace(query.Get("destination_ref")); destinationRef != "" {
|
||||
req.DestinationRef = destinationRef
|
||||
}
|
||||
|
||||
if states, err := parsePaymentStateFilters(r); err != nil {
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
} else if len(states) > 0 {
|
||||
req.FilterStates = states
|
||||
}
|
||||
|
||||
resp, err := a.client.ListPayments(ctx, req)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to list payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
return sresponse.PaymentsListResponse(a.logger, resp, token)
|
||||
}
|
||||
|
||||
func listPaymentsPage(r *http.Request) (*paginationv1.CursorPageRequest, error) {
|
||||
query := r.URL.Query()
|
||||
cursor := strings.TrimSpace(query.Get("cursor"))
|
||||
limitRaw := strings.TrimSpace(query.Get("limit"))
|
||||
|
||||
var limit int64
|
||||
hasLimit := false
|
||||
if limitRaw != "" {
|
||||
parsed, err := strconv.ParseInt(limitRaw, 10, 32)
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid limit", "limit")
|
||||
}
|
||||
limit = parsed
|
||||
hasLimit = true
|
||||
}
|
||||
|
||||
if cursor == "" && !hasLimit {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
page := &paginationv1.CursorPageRequest{
|
||||
Cursor: cursor,
|
||||
}
|
||||
if hasLimit {
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
} else if limit > maxInt32 {
|
||||
limit = maxInt32
|
||||
}
|
||||
page.Limit = int32(limit)
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func parsePaymentStateFilters(r *http.Request) ([]orchestratorv1.PaymentState, error) {
|
||||
query := r.URL.Query()
|
||||
values := append([]string{}, query["state"]...)
|
||||
values = append(values, query["states"]...)
|
||||
values = append(values, query["filter_states"]...)
|
||||
if len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
states := make([]orchestratorv1.PaymentState, 0, len(values))
|
||||
for _, raw := range values {
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
state, ok := paymentStateFromString(trimmed)
|
||||
if !ok {
|
||||
return nil, merrors.InvalidArgument("unsupported payment state: "+trimmed, "state")
|
||||
}
|
||||
states = append(states, state)
|
||||
}
|
||||
}
|
||||
|
||||
if len(states) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return states, nil
|
||||
}
|
||||
|
||||
func paymentStateFromString(value string) (orchestratorv1.PaymentState, bool) {
|
||||
upper := strings.ToUpper(strings.TrimSpace(value))
|
||||
if upper == "" {
|
||||
return 0, false
|
||||
}
|
||||
if !strings.HasPrefix(upper, "PAYMENT_STATE_") {
|
||||
upper = "PAYMENT_STATE_" + upper
|
||||
}
|
||||
enumValue, ok := orchestratorv1.PaymentState_value[upper]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return orchestratorv1.PaymentState(enumValue), true
|
||||
}
|
||||
@@ -50,7 +50,6 @@ func mapPaymentIntent(intent *srequest.PaymentIntent) (*orchestratorv1.PaymentIn
|
||||
Fx: fx,
|
||||
SettlementMode: settlementMode,
|
||||
Attributes: copyStringMap(intent.Attributes),
|
||||
Customer: mapCustomer(intent.Customer),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -201,24 +200,6 @@ func mapFXIntent(fx *srequest.FXIntent) (*orchestratorv1.FXIntent, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapCustomer(customer *srequest.Customer) *orchestratorv1.Customer {
|
||||
if customer == nil {
|
||||
return nil
|
||||
}
|
||||
return &orchestratorv1.Customer{
|
||||
Id: strings.TrimSpace(customer.ID),
|
||||
FirstName: strings.TrimSpace(customer.FirstName),
|
||||
MiddleName: strings.TrimSpace(customer.MiddleName),
|
||||
LastName: strings.TrimSpace(customer.LastName),
|
||||
Ip: strings.TrimSpace(customer.IP),
|
||||
Zip: strings.TrimSpace(customer.Zip),
|
||||
Country: strings.TrimSpace(customer.Country),
|
||||
State: strings.TrimSpace(customer.State),
|
||||
City: strings.TrimSpace(customer.City),
|
||||
Address: strings.TrimSpace(customer.Address),
|
||||
}
|
||||
}
|
||||
|
||||
func mapCurrencyPair(pair *srequest.CurrencyPair) *fxv1.CurrencyPair {
|
||||
if pair == nil {
|
||||
return nil
|
||||
|
||||
@@ -59,7 +59,6 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
|
||||
|
||||
var intent *orchestratorv1.PaymentIntent
|
||||
if payload.Intent != nil {
|
||||
applyCustomerIP(payload.Intent, r.RemoteAddr)
|
||||
intent, err = mapPaymentIntent(payload.Intent)
|
||||
if err != nil {
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
|
||||
@@ -44,7 +44,6 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
applyCustomerIP(&payload.Intent, r.RemoteAddr)
|
||||
intent, err := mapPaymentIntent(&payload.Intent)
|
||||
if err != nil {
|
||||
a.logger.Debug("Failed to map payment intent", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
@@ -98,7 +97,6 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
|
||||
|
||||
intents := make([]*orchestratorv1.PaymentIntent, 0, len(payload.Intents))
|
||||
for i := range payload.Intents {
|
||||
applyCustomerIP(&payload.Intents[i], r.RemoteAddr)
|
||||
intent, err := mapPaymentIntent(&payload.Intents[i])
|
||||
if err != nil {
|
||||
a.logger.Debug("Failed to map payment intent", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
|
||||
@@ -25,7 +25,6 @@ type paymentClient interface {
|
||||
QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
|
||||
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
|
||||
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
||||
ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -73,7 +72,6 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/immediate"), api.Post, p.initiateImmediate)
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-quote"), api.Post, p.initiateByQuote)
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-multiquote"), api.Post, p.initiatePaymentsByQuote)
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listPayments)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/api/responses/base.dart';
|
||||
import 'package:pshared/api/responses/token.dart';
|
||||
import 'package:pshared/data/dto/payment/payment.dart';
|
||||
|
||||
part 'payments.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class PaymentsResponse extends BaseAuthorizedResponse {
|
||||
|
||||
final List<PaymentDTO> payments;
|
||||
|
||||
const PaymentsResponse({required super.accessToken, required this.payments});
|
||||
|
||||
factory PaymentsResponse.fromJson(Map<String, dynamic> json) => _$PaymentsResponseFromJson(json);
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$PaymentsResponseToJson(this);
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'package:pshared/api/requests/payment/initiate.dart';
|
||||
import 'package:pshared/api/responses/payment/payment.dart';
|
||||
import 'package:pshared/api/responses/payment/payments.dart';
|
||||
import 'package:pshared/data/mapper/payment/payment_response.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/service/authorization/service.dart';
|
||||
@@ -15,40 +13,6 @@ class PaymentService {
|
||||
static final _logger = Logger('service.payment');
|
||||
static const String _objectType = Services.payments;
|
||||
|
||||
static Future<List<Payment>> list(
|
||||
String organizationRef, {
|
||||
int? limit,
|
||||
String? cursor,
|
||||
String? sourceRef,
|
||||
String? destinationRef,
|
||||
List<String>? states,
|
||||
}) async {
|
||||
_logger.fine('Listing payments for organization $organizationRef');
|
||||
final queryParams = <String, String>{};
|
||||
if (limit != null) {
|
||||
queryParams['limit'] = limit.toString();
|
||||
}
|
||||
if (cursor != null && cursor.isNotEmpty) {
|
||||
queryParams['cursor'] = cursor;
|
||||
}
|
||||
if (sourceRef != null && sourceRef.isNotEmpty) {
|
||||
queryParams['source_ref'] = sourceRef;
|
||||
}
|
||||
if (destinationRef != null && destinationRef.isNotEmpty) {
|
||||
queryParams['destination_ref'] = destinationRef;
|
||||
}
|
||||
if (states != null && states.isNotEmpty) {
|
||||
queryParams['state'] = states.join(',');
|
||||
}
|
||||
|
||||
final path = '/$organizationRef';
|
||||
final url = queryParams.isEmpty
|
||||
? path
|
||||
: Uri(path: path, queryParameters: queryParams).toString();
|
||||
final response = await AuthorizationService.getGETResponse(_objectType, url);
|
||||
return PaymentsResponse.fromJson(response).payments.map((payment) => payment.toDomain()).toList();
|
||||
}
|
||||
|
||||
static Future<Payment> pay(
|
||||
String organizationRef,
|
||||
String quotationRef, {
|
||||
|
||||
@@ -99,7 +99,6 @@ void main() async {
|
||||
methods,
|
||||
),
|
||||
),
|
||||
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => OperationProvider(OperationService())..loadOperations(),
|
||||
),
|
||||
|
||||
11
frontend/pweb/lib/pages/dashboard/payouts/widget.dart
Normal file
11
frontend/pweb/lib/pages/dashboard/payouts/widget.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/form.dart';
|
||||
|
||||
|
||||
class PaymentFromWrappingWidget extends StatelessWidget {
|
||||
const PaymentFromWrappingWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const PaymentFormWidget();
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/form.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/widget.dart';
|
||||
import 'package:pweb/pages/payment_methods/payment_page/back_button.dart';
|
||||
import 'package:pweb/pages/payment_methods/payment_page/header.dart';
|
||||
import 'package:pweb/pages/payment_methods/payment_page/method_selector.dart';
|
||||
@@ -93,7 +93,7 @@ class PaymentPageContent extends StatelessWidget {
|
||||
SizedBox(height: dimensions.paddingXLarge),
|
||||
PaymentInfoSection(dimensions: dimensions),
|
||||
SizedBox(height: dimensions.paddingLarge),
|
||||
const PaymentFormWidget(),
|
||||
const PaymentFromWrappingWidget(),
|
||||
SizedBox(height: dimensions.paddingXXXLarge),
|
||||
SendButton(onPressed: onSend),
|
||||
SizedBox(height: dimensions.paddingLarge),
|
||||
|
||||
@@ -52,7 +52,7 @@ class WalletTopUpAddressBlock extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.4),
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.4),
|
||||
),
|
||||
child: SelectableText(
|
||||
address,
|
||||
@@ -72,7 +72,7 @@ class WalletTopUpAddressBlock extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.4),
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.4),
|
||||
),
|
||||
child: Center(
|
||||
child: QrImageView(
|
||||
|
||||
@@ -23,7 +23,7 @@ class WalletTopUpInfoChip extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.4),
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.4),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
24
frontend/pweb/lib/providers/mock_payment.dart
Normal file
24
frontend/pweb/lib/providers/mock_payment.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class MockPaymentProvider with ChangeNotifier {
|
||||
double _amount = 10.0;
|
||||
bool _payerCoversFee = true;
|
||||
|
||||
double get amount => _amount;
|
||||
bool get payerCoversFee => _payerCoversFee;
|
||||
|
||||
double get fee => _amount * 0.05;
|
||||
double get total => payerCoversFee ? (_amount + fee) : _amount;
|
||||
double get recipientGets => payerCoversFee ? _amount : (_amount - fee);
|
||||
|
||||
void setAmount(double value) {
|
||||
_amount = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPayerCoversFee(bool value) {
|
||||
_payerCoversFee = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user