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 } }