214 lines
9.3 KiB
Go
214 lines
9.3 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/tech/sendico/chain/gateway/storage/model"
|
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
func (s *Service) createManagedWalletHandler(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) gsresponse.Responder[gatewayv1.CreateManagedWalletResponse] {
|
|
if err := s.ensureRepository(ctx); err != nil {
|
|
return gsresponse.Unavailable[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
if req == nil {
|
|
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
}
|
|
|
|
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
|
if idempotencyKey == "" {
|
|
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
|
}
|
|
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
|
if organizationRef == "" {
|
|
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
|
}
|
|
ownerRef := strings.TrimSpace(req.GetOwnerRef())
|
|
if ownerRef == "" {
|
|
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("owner_ref is required"))
|
|
}
|
|
|
|
asset := req.GetAsset()
|
|
if asset == nil {
|
|
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
|
|
}
|
|
|
|
chainKey, _ := chainKeyFromEnum(asset.GetChain())
|
|
if chainKey == "" {
|
|
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
|
}
|
|
networkCfg, ok := s.networks[chainKey]
|
|
if !ok {
|
|
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
|
}
|
|
|
|
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
|
if tokenSymbol == "" {
|
|
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required"))
|
|
}
|
|
contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress()))
|
|
if contractAddress == "" {
|
|
contractAddress = resolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
|
|
if contractAddress == "" {
|
|
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
|
|
}
|
|
}
|
|
|
|
walletRef := generateWalletRef()
|
|
if s.keyManager == nil {
|
|
return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager not configured"))
|
|
}
|
|
|
|
keyInfo, err := s.keyManager.CreateManagedWalletKey(ctx, walletRef, chainKey)
|
|
if err != nil {
|
|
return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
if keyInfo == nil || strings.TrimSpace(keyInfo.Address) == "" {
|
|
return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
|
|
}
|
|
|
|
wallet := &model.ManagedWallet{
|
|
IdempotencyKey: idempotencyKey,
|
|
WalletRef: walletRef,
|
|
OrganizationRef: organizationRef,
|
|
OwnerRef: ownerRef,
|
|
Network: chainKey,
|
|
TokenSymbol: tokenSymbol,
|
|
ContractAddress: contractAddress,
|
|
DepositAddress: strings.ToLower(keyInfo.Address),
|
|
KeyReference: keyInfo.KeyID,
|
|
Status: model.ManagedWalletStatusActive,
|
|
Metadata: cloneMetadata(req.GetMetadata()),
|
|
}
|
|
|
|
created, err := s.storage.Wallets().Create(ctx, wallet)
|
|
if err != nil {
|
|
if errors.Is(err, merrors.ErrDataConflict) {
|
|
s.logger.Debug("wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey))
|
|
return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)})
|
|
}
|
|
return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
|
|
return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)})
|
|
}
|
|
|
|
func (s *Service) getManagedWalletHandler(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) gsresponse.Responder[gatewayv1.GetManagedWalletResponse] {
|
|
if err := s.ensureRepository(ctx); err != nil {
|
|
return gsresponse.Unavailable[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
if req == nil {
|
|
return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
}
|
|
walletRef := strings.TrimSpace(req.GetWalletRef())
|
|
if walletRef == "" {
|
|
return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
|
}
|
|
wallet, err := s.storage.Wallets().Get(ctx, walletRef)
|
|
if err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
return gsresponse.NotFound[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
return gsresponse.Auto[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
return gsresponse.Success(&gatewayv1.GetManagedWalletResponse{Wallet: s.toProtoManagedWallet(wallet)})
|
|
}
|
|
|
|
func (s *Service) listManagedWalletsHandler(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) gsresponse.Responder[gatewayv1.ListManagedWalletsResponse] {
|
|
if err := s.ensureRepository(ctx); err != nil {
|
|
return gsresponse.Unavailable[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
filter := model.ManagedWalletFilter{}
|
|
if req != nil {
|
|
filter.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
|
filter.OwnerRef = strings.TrimSpace(req.GetOwnerRef())
|
|
if asset := req.GetAsset(); asset != nil {
|
|
filter.Network, _ = chainKeyFromEnum(asset.GetChain())
|
|
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
|
|
}
|
|
if page := req.GetPage(); page != nil {
|
|
filter.Cursor = strings.TrimSpace(page.GetCursor())
|
|
filter.Limit = page.GetLimit()
|
|
}
|
|
}
|
|
|
|
result, err := s.storage.Wallets().List(ctx, filter)
|
|
if err != nil {
|
|
return gsresponse.Auto[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
|
|
protoWallets := make([]*gatewayv1.ManagedWallet, 0, len(result.Items))
|
|
for _, wallet := range result.Items {
|
|
protoWallets = append(protoWallets, s.toProtoManagedWallet(wallet))
|
|
}
|
|
|
|
resp := &gatewayv1.ListManagedWalletsResponse{
|
|
Wallets: protoWallets,
|
|
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
|
|
}
|
|
return gsresponse.Success(resp)
|
|
}
|
|
|
|
func (s *Service) getWalletBalanceHandler(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) gsresponse.Responder[gatewayv1.GetWalletBalanceResponse] {
|
|
if err := s.ensureRepository(ctx); err != nil {
|
|
return gsresponse.Unavailable[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
if req == nil {
|
|
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
}
|
|
walletRef := strings.TrimSpace(req.GetWalletRef())
|
|
if walletRef == "" {
|
|
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
|
}
|
|
balance, err := s.storage.Wallets().GetBalance(ctx, walletRef)
|
|
if err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
return gsresponse.NotFound[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
return gsresponse.Auto[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
|
|
}
|
|
return gsresponse.Success(&gatewayv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(balance)})
|
|
}
|
|
|
|
func (s *Service) toProtoManagedWallet(wallet *model.ManagedWallet) *gatewayv1.ManagedWallet {
|
|
if wallet == nil {
|
|
return nil
|
|
}
|
|
asset := &gatewayv1.Asset{
|
|
Chain: chainEnumFromName(wallet.Network),
|
|
TokenSymbol: wallet.TokenSymbol,
|
|
ContractAddress: wallet.ContractAddress,
|
|
}
|
|
return &gatewayv1.ManagedWallet{
|
|
WalletRef: wallet.WalletRef,
|
|
OrganizationRef: wallet.OrganizationRef,
|
|
OwnerRef: wallet.OwnerRef,
|
|
Asset: asset,
|
|
DepositAddress: wallet.DepositAddress,
|
|
Status: managedWalletStatusToProto(wallet.Status),
|
|
Metadata: cloneMetadata(wallet.Metadata),
|
|
CreatedAt: timestamppb.New(wallet.CreatedAt.UTC()),
|
|
UpdatedAt: timestamppb.New(wallet.UpdatedAt.UTC()),
|
|
}
|
|
}
|
|
|
|
func toProtoWalletBalance(balance *model.WalletBalance) *gatewayv1.WalletBalance {
|
|
if balance == nil {
|
|
return nil
|
|
}
|
|
return &gatewayv1.WalletBalance{
|
|
Available: cloneMoney(balance.Available),
|
|
PendingInbound: cloneMoney(balance.PendingInbound),
|
|
PendingOutbound: cloneMoney(balance.PendingOutbound),
|
|
CalculatedAt: timestamppb.New(balance.CalculatedAt.UTC()),
|
|
}
|
|
}
|