Files
sendico/api/chain/gateway/internal/service/gateway/service.go
Stephan D 2ee17b0c46
Some checks failed
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx/1 Pipeline failed
ci/woodpecker/push/fx/2 Pipeline failed
ci/woodpecker/push/nats Pipeline was successful
fx build fix
2025-11-07 23:50:48 +01:00

215 lines
7.2 KiB
Go

package gateway
import (
"context"
"strings"
"github.com/tech/sendico/chain/gateway/internal/keymanager"
"github.com/tech/sendico/chain/gateway/storage"
"github.com/tech/sendico/chain/gateway/storage/model"
"github.com/tech/sendico/pkg/api/routers"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
clockpkg "github.com/tech/sendico/pkg/clock"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
"go.mongodb.org/mongo-driver/bson/primitive"
"google.golang.org/grpc"
)
type serviceError string
func (e serviceError) Error() string {
return string(e)
}
var (
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
)
// Service implements the ChainGatewayService RPC contract.
type Service struct {
logger mlogger.Logger
storage storage.Repository
producer msg.Producer
clock clockpkg.Clock
networks map[string]Network
serviceWallet ServiceWallet
keyManager keymanager.Manager
executor TransferExecutor
gatewayv1.UnimplementedChainGatewayServiceServer
}
// NewService constructs the chain gateway service skeleton.
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
svc := &Service{
logger: logger.Named("chain_gateway"),
storage: repo,
producer: producer,
clock: clockpkg.System{},
networks: map[string]Network{},
}
initMetrics()
for _, opt := range opts {
if opt != nil {
opt(svc)
}
}
if svc.clock == nil {
svc.clock = clockpkg.System{}
}
if svc.networks == nil {
svc.networks = map[string]Network{}
}
return svc
}
// Register wires the service onto the provided gRPC router.
func (s *Service) Register(router routers.GRPC) error {
return router.Register(func(reg grpc.ServiceRegistrar) {
gatewayv1.RegisterChainGatewayServiceServer(reg, s)
})
}
func (s *Service) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) {
return executeUnary(ctx, s, "CreateManagedWallet", s.createManagedWalletHandler, req)
}
func (s *Service) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) {
return executeUnary(ctx, s, "GetManagedWallet", s.getManagedWalletHandler, req)
}
func (s *Service) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) {
return executeUnary(ctx, s, "ListManagedWallets", s.listManagedWalletsHandler, req)
}
func (s *Service) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) {
return executeUnary(ctx, s, "GetWalletBalance", s.getWalletBalanceHandler, req)
}
func (s *Service) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
return executeUnary(ctx, s, "SubmitTransfer", s.submitTransferHandler, req)
}
func (s *Service) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) {
return executeUnary(ctx, s, "GetTransfer", s.getTransferHandler, req)
}
func (s *Service) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) {
return executeUnary(ctx, s, "ListTransfers", s.listTransfersHandler, req)
}
func (s *Service) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) {
return executeUnary(ctx, s, "EstimateTransferFee", s.estimateTransferFeeHandler, req)
}
func (s *Service) ensureRepository(ctx context.Context) error {
if s.storage == nil {
return errStorageUnavailable
}
return s.storage.Ping(ctx)
}
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) {
start := svc.clock.Now()
resp, err := gsresponse.Unary(svc.logger, mservice.ChainGateway, handler)(ctx, req)
observeRPC(method, err, svc.clock.Now().Sub(start))
return resp, err
}
func resolveContractAddress(tokens []TokenContract, symbol string) string {
upper := strings.ToUpper(symbol)
for _, token := range tokens {
if strings.EqualFold(token.Symbol, upper) && token.ContractAddress != "" {
return strings.ToLower(token.ContractAddress)
}
}
return ""
}
func generateWalletRef() string {
return primitive.NewObjectID().Hex()
}
func generateTransferRef() string {
return primitive.NewObjectID().Hex()
}
func chainKeyFromEnum(chain gatewayv1.ChainNetwork) (string, gatewayv1.ChainNetwork) {
if name, ok := gatewayv1.ChainNetwork_name[int32(chain)]; ok {
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
return key, chain
}
return "", gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
func chainEnumFromName(name string) gatewayv1.ChainNetwork {
if name == "" {
return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
upper := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, " ", "_"), "-", "_"))
key := "CHAIN_NETWORK_" + upper
if val, ok := gatewayv1.ChainNetwork_value[key]; ok {
return gatewayv1.ChainNetwork(val)
}
return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
func managedWalletStatusToProto(status model.ManagedWalletStatus) gatewayv1.ManagedWalletStatus {
switch status {
case model.ManagedWalletStatusActive:
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
case model.ManagedWalletStatusSuspended:
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
case model.ManagedWalletStatusClosed:
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
default:
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
}
}
func transferStatusToModel(status gatewayv1.TransferStatus) model.TransferStatus {
switch status {
case gatewayv1.TransferStatus_TRANSFER_PENDING:
return model.TransferStatusPending
case gatewayv1.TransferStatus_TRANSFER_SIGNING:
return model.TransferStatusSigning
case gatewayv1.TransferStatus_TRANSFER_SUBMITTED:
return model.TransferStatusSubmitted
case gatewayv1.TransferStatus_TRANSFER_CONFIRMED:
return model.TransferStatusConfirmed
case gatewayv1.TransferStatus_TRANSFER_FAILED:
return model.TransferStatusFailed
case gatewayv1.TransferStatus_TRANSFER_CANCELLED:
return model.TransferStatusCancelled
default:
return ""
}
}
func transferStatusToProto(status model.TransferStatus) gatewayv1.TransferStatus {
switch status {
case model.TransferStatusPending:
return gatewayv1.TransferStatus_TRANSFER_PENDING
case model.TransferStatusSigning:
return gatewayv1.TransferStatus_TRANSFER_SIGNING
case model.TransferStatusSubmitted:
return gatewayv1.TransferStatus_TRANSFER_SUBMITTED
case model.TransferStatusConfirmed:
return gatewayv1.TransferStatus_TRANSFER_CONFIRMED
case model.TransferStatusFailed:
return gatewayv1.TransferStatus_TRANSFER_FAILED
case model.TransferStatusCancelled:
return gatewayv1.TransferStatus_TRANSFER_CANCELLED
default:
return gatewayv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
}
}