237 lines
7.1 KiB
Go
237 lines
7.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tech/sendico/chain/gateway/storage"
|
|
"github.com/tech/sendico/chain/gateway/storage/model"
|
|
"github.com/tech/sendico/pkg/db/repository"
|
|
"github.com/tech/sendico/pkg/db/repository/builder"
|
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
"github.com/tech/sendico/pkg/mlogger"
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
"go.mongodb.org/mongo-driver/mongo"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
defaultWalletPageSize int64 = 50
|
|
maxWalletPageSize int64 = 200
|
|
)
|
|
|
|
type Wallets struct {
|
|
logger mlogger.Logger
|
|
walletRepo repository.Repository
|
|
balanceRepo repository.Repository
|
|
}
|
|
|
|
// NewWallets constructs a Mongo-backed wallets store.
|
|
func NewWallets(logger mlogger.Logger, db *mongo.Database) (*Wallets, error) {
|
|
if db == nil {
|
|
return nil, merrors.InvalidArgument("mongo database is nil")
|
|
}
|
|
|
|
walletRepo := repository.CreateMongoRepository(db, mservice.ChainWallets)
|
|
walletIndexes := []*ri.Definition{
|
|
{
|
|
Keys: []ri.Key{{Field: "walletRef", Sort: ri.Asc}},
|
|
Unique: true,
|
|
},
|
|
{
|
|
Keys: []ri.Key{{Field: "idempotencyKey", Sort: ri.Asc}},
|
|
Unique: true,
|
|
},
|
|
{
|
|
Keys: []ri.Key{{Field: "depositAddress", Sort: ri.Asc}},
|
|
Unique: true,
|
|
},
|
|
{
|
|
Keys: []ri.Key{{Field: "organizationRef", Sort: ri.Asc}, {Field: "ownerRef", Sort: ri.Asc}},
|
|
},
|
|
}
|
|
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))
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
balanceRepo := repository.CreateMongoRepository(db, mservice.ChainWalletBalances)
|
|
balanceIndexes := []*ri.Definition{
|
|
{
|
|
Keys: []ri.Key{{Field: "walletRef", Sort: ri.Asc}},
|
|
Unique: true,
|
|
},
|
|
}
|
|
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))
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
childLogger := logger.Named("wallets")
|
|
childLogger.Debug("wallet stores initialised")
|
|
|
|
return &Wallets{
|
|
logger: childLogger,
|
|
walletRepo: walletRepo,
|
|
balanceRepo: balanceRepo,
|
|
}, nil
|
|
}
|
|
|
|
func (w *Wallets) Create(ctx context.Context, wallet *model.ManagedWallet) (*model.ManagedWallet, error) {
|
|
if wallet == nil {
|
|
return nil, merrors.InvalidArgument("walletsStore: nil wallet")
|
|
}
|
|
wallet.Normalize()
|
|
if strings.TrimSpace(wallet.WalletRef) == "" {
|
|
return nil, merrors.InvalidArgument("walletsStore: empty walletRef")
|
|
}
|
|
if wallet.Status == "" {
|
|
wallet.Status = model.ManagedWalletStatusActive
|
|
}
|
|
if strings.TrimSpace(wallet.IdempotencyKey) == "" {
|
|
return nil, merrors.InvalidArgument("walletsStore: empty idempotencyKey")
|
|
}
|
|
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", zap.String("wallet_ref", wallet.WalletRef), zap.String("idempotency_key", wallet.IdempotencyKey))
|
|
return wallet, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
w.logger.Debug("wallet created", zap.String("wallet_ref", wallet.WalletRef))
|
|
return wallet, nil
|
|
}
|
|
|
|
func (w *Wallets) Get(ctx context.Context, walletRef string) (*model.ManagedWallet, error) {
|
|
walletRef = strings.TrimSpace(walletRef)
|
|
if walletRef == "" {
|
|
return nil, merrors.InvalidArgument("walletsStore: empty walletRef")
|
|
}
|
|
wallet := &model.ManagedWallet{}
|
|
if err := w.walletRepo.FindOneByFilter(ctx, repository.Filter("walletRef", walletRef), wallet); err != nil {
|
|
return nil, err
|
|
}
|
|
return wallet, nil
|
|
}
|
|
|
|
func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (*model.ManagedWalletList, error) {
|
|
query := repository.Query()
|
|
|
|
if org := strings.TrimSpace(filter.OrganizationRef); org != "" {
|
|
query = query.Filter(repository.Field("organizationRef"), org)
|
|
}
|
|
if owner := strings.TrimSpace(filter.OwnerRef); owner != "" {
|
|
query = query.Filter(repository.Field("ownerRef"), owner)
|
|
}
|
|
if network := strings.TrimSpace(filter.Network); network != "" {
|
|
query = query.Filter(repository.Field("network"), strings.ToLower(network))
|
|
}
|
|
if token := strings.TrimSpace(filter.TokenSymbol); token != "" {
|
|
query = query.Filter(repository.Field("tokenSymbol"), strings.ToUpper(token))
|
|
}
|
|
|
|
if cursor := strings.TrimSpace(filter.Cursor); cursor != "" {
|
|
if oid, err := primitive.ObjectIDFromHex(cursor); err == nil {
|
|
query = query.Comparison(repository.IDField(), builder.Gt, oid)
|
|
} else {
|
|
w.logger.Warn("ignoring invalid wallet cursor", zap.String("cursor", cursor), zap.Error(err))
|
|
}
|
|
}
|
|
|
|
limit := sanitizeWalletLimit(filter.Limit)
|
|
fetchLimit := limit + 1
|
|
query = query.Sort(repository.IDField(), true).Limit(&fetchLimit)
|
|
|
|
wallets := make([]*model.ManagedWallet, 0, fetchLimit)
|
|
decoder := func(cur *mongo.Cursor) error {
|
|
item := &model.ManagedWallet{}
|
|
if err := cur.Decode(item); err != nil {
|
|
return err
|
|
}
|
|
wallets = append(wallets, item)
|
|
return nil
|
|
}
|
|
|
|
if err := w.walletRepo.FindManyByFilter(ctx, query, decoder); err != nil && !errors.Is(err, merrors.ErrNoData) {
|
|
return nil, err
|
|
}
|
|
|
|
nextCursor := ""
|
|
if int64(len(wallets)) == fetchLimit {
|
|
last := wallets[len(wallets)-1]
|
|
nextCursor = last.ID.Hex()
|
|
wallets = wallets[:len(wallets)-1]
|
|
}
|
|
|
|
return &model.ManagedWalletList{
|
|
Items: wallets,
|
|
NextCursor: nextCursor,
|
|
}, nil
|
|
}
|
|
|
|
func (w *Wallets) SaveBalance(ctx context.Context, balance *model.WalletBalance) error {
|
|
if balance == nil {
|
|
return merrors.InvalidArgument("walletsStore: nil balance")
|
|
}
|
|
balance.Normalize()
|
|
if strings.TrimSpace(balance.WalletRef) == "" {
|
|
return merrors.InvalidArgument("walletsStore: empty walletRef for balance")
|
|
}
|
|
if balance.CalculatedAt.IsZero() {
|
|
balance.CalculatedAt = time.Now().UTC()
|
|
}
|
|
|
|
existing := &model.WalletBalance{}
|
|
err := w.balanceRepo.FindOneByFilter(ctx, repository.Filter("walletRef", balance.WalletRef), existing)
|
|
switch {
|
|
case err == nil:
|
|
existing.Available = balance.Available
|
|
existing.PendingInbound = balance.PendingInbound
|
|
existing.PendingOutbound = balance.PendingOutbound
|
|
existing.CalculatedAt = balance.CalculatedAt
|
|
if err := w.balanceRepo.Update(ctx, existing); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
case errors.Is(err, merrors.ErrNoData):
|
|
if err := w.balanceRepo.Insert(ctx, balance, repository.Filter("walletRef", balance.WalletRef)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (w *Wallets) GetBalance(ctx context.Context, walletRef string) (*model.WalletBalance, error) {
|
|
walletRef = strings.TrimSpace(walletRef)
|
|
if walletRef == "" {
|
|
return nil, merrors.InvalidArgument("walletsStore: empty walletRef")
|
|
}
|
|
balance := &model.WalletBalance{}
|
|
if err := w.balanceRepo.FindOneByFilter(ctx, repository.Filter("walletRef", walletRef), balance); err != nil {
|
|
return nil, err
|
|
}
|
|
return balance, nil
|
|
}
|
|
|
|
func sanitizeWalletLimit(requested int32) int64 {
|
|
if requested <= 0 {
|
|
return defaultWalletPageSize
|
|
}
|
|
if requested > int32(maxWalletPageSize) {
|
|
return maxWalletPageSize
|
|
}
|
|
return int64(requested)
|
|
}
|
|
|
|
var _ storage.WalletsStore = (*Wallets)(nil)
|