package wallet import ( "context" "errors" "strings" "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" "github.com/tech/sendico/gateway/chain/storage/model" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" pkgmodel "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" "go.uber.org/zap" ) type createManagedWalletCommand struct { deps Deps } func NewCreateManagedWallet(deps Deps) *createManagedWalletCommand { return &createManagedWalletCommand{deps: deps} } 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)) return gsresponse.Unavailable[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err) } if req == nil { 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") 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") 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") 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") 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())) 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)) 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") return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required")) } contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress())) if contractAddress == "" { contractAddress = shared.ResolveContractAddress(networkCfg.TokenConfigs, tokenSymbol) if contractAddress == "" { 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")) } } walletRef := shared.GenerateWalletRef() if c.deps.KeyManager == nil { 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)) 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") return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address")) } metadata := shared.CloneMetadata(req.GetMetadata()) desc := req.GetDescribable() name := strings.TrimSpace(desc.GetName()) if name == "" { name = strings.TrimSpace(metadata["name"]) } var description *string if desc != nil && desc.Description != nil { if trimmed := strings.TrimSpace(desc.GetDescription()); trimmed != "" { description = &trimmed } } if description == nil { if trimmed := strings.TrimSpace(metadata["description"]); trimmed != "" { description = &trimmed } } if name == "" { name = walletRef } wallet := &model.ManagedWallet{ Describable: pkgmodel.Describable{ Name: name, }, 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: metadata, } if description != nil { wallet.Describable.Description = description } 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)) return gsresponse.Success(&chainv1.CreateManagedWalletResponse{Wallet: toProtoManagedWallet(created)}) } 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) } return gsresponse.Success(&chainv1.CreateManagedWalletResponse{Wallet: toProtoManagedWallet(created)}) }