package walletapiimp import ( "context" "crypto/tls" "encoding/json" "net/http" "strings" "github.com/google/uuid" "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/discovery" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mutil/mzap" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" "github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/sresponse" mutil "github.com/tech/sendico/server/internal/mutil/param" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/types/known/structpb" ) func (a *WalletAPI) create(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc { orgRef, err := a.oph.GetRef(r) if err != nil { a.logger.Warn("Failed to parse organization reference for wallet list", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r))) return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err) } var sr srequest.CreateWallet if err := json.NewDecoder(r.Body).Decode(&sr); err != nil { a.logger.Warn("Failed to decode wallet creation request request", zap.Error(err), mzap.StorableRef(account)) return response.BadPayload(a.logger, a.Name(), err) } ctx := r.Context() res, err := a.enf.Enforce(ctx, a.walletsPermissionRef, account.ID, orgRef, bson.NilObjectID, model.ActionCreate) if err != nil { a.logger.Warn("Failed to check chain wallet access permissions", zap.Error(err), mutil.PLog(a.oph, r), mzap.StorableRef(account)) return response.Auto(a.logger, a.Name(), err) } if !res { a.logger.Debug("Access denied when listing organization wallets", mutil.PLog(a.oph, r), mzap.StorableRef(account)) return response.AccessDenied(a.logger, a.Name(), "wallets creation permission denied") } asset, err := a.assets.Resolve(ctx, sr.Asset) if err != nil { a.logger.Warn("Failed to resolve asset", zap.Error(err), mzap.StorableRef(account), zap.String("chain", string(sr.Asset.Chain)), zap.String("token", sr.Asset.TokenSymbol)) return response.Auto(a.logger, a.Name(), err) } if a.discovery == nil { return response.Internal(a.logger, mservice.ChainGateway, merrors.Internal("discovery client is not configured")) } // Find gateway for this network lookupCtx, cancel := context.WithTimeout(ctx, discoveryLookupTimeout) defer cancel() lookupResp, err := a.discovery.Lookup(lookupCtx) if err != nil { a.logger.Warn("Failed to lookup discovery registry", zap.Error(err)) return response.Auto(a.logger, a.Name(), err) } // Find gateway that handles this network networkName := strings.ToLower(string(asset.Asset.Chain)) gateway := findGatewayForNetwork(lookupResp.Gateways, networkName) if gateway == nil { a.logger.Warn("No gateway found for network", zap.String("network", networkName), zap.String("chain", string(sr.Asset.Chain))) return response.Auto(a.logger, a.Name(), merrors.InvalidArgument("no gateway available for network: "+networkName)) } a.logger.Debug("Selected gateway for wallet creation", mzap.ObjRef("organization_ref", orgRef), zap.String("network", networkName), zap.String("gateway_id", gateway.ID), zap.String("gateway_network", gateway.Network), zap.String("invoke_uri", gateway.InvokeURI)) var ownerRef string if sr.OwnerRef != nil && !sr.OwnerRef.IsZero() { ownerRef = sr.OwnerRef.Hex() } // Build params for connector OpenAccount params := map[string]interface{}{ "organization_ref": orgRef.Hex(), "network": networkName, "token_symbol": asset.Asset.TokenSymbol, "contract_address": asset.Asset.ContractAddress, } if sr.Description.Description != nil { params["description"] = *sr.Description.Description } params["metadata"] = map[string]interface{}{ "source": "create", "login": account.Login, } paramsStruct, _ := structpb.NewStruct(params) assetString := networkName + "-" + asset.Asset.TokenSymbol req := &connectorv1.OpenAccountRequest{ IdempotencyKey: uuid.NewString(), Kind: connectorv1.AccountKind_CHAIN_MANAGED_WALLET, Asset: assetString, OwnerRef: ownerRef, Label: sr.Description.Name, Params: paramsStruct, } // Connect to gateway and create wallet walletRef, err := a.createWalletOnGateway(ctx, *gateway, req) if err != nil { a.logger.Warn("Failed to create managed wallet", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), mzap.StorableRef(account), zap.String("gateway_id", gateway.ID), zap.String("network", gateway.Network)) return response.Auto(a.logger, a.Name(), err) } a.logger.Info("Managed wallet created for organization", mzap.ObjRef("organization_ref", orgRef), zap.String("wallet_ref", walletRef), mzap.StorableRef(account), zap.String("gateway_id", gateway.ID), zap.String("network", gateway.Network)) a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, networkName, gateway.ID) a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, gateway.Network, gateway.ID) a.logger.Debug("Persisted wallet route after wallet creation", mzap.ObjRef("organization_ref", orgRef), zap.String("wallet_ref", walletRef), zap.String("network", networkName), zap.String("gateway_id", gateway.ID)) return sresponse.Success(a.logger, token) } func findGatewayForNetwork(gateways []discovery.GatewaySummary, network string) *discovery.GatewaySummary { network = strings.ToLower(strings.TrimSpace(network)) for _, gw := range gateways { if !strings.EqualFold(gw.Rail, cryptoRail) || !gw.Healthy || strings.TrimSpace(gw.InvokeURI) == "" { continue } // Check if gateway network matches gwNetwork := strings.ToLower(strings.TrimSpace(gw.Network)) if gwNetwork == network { return &gw } // Also check if network starts with gateway network prefix (e.g., "tron" matches "tron_mainnet") if strings.HasPrefix(network, gwNetwork) || strings.HasPrefix(gwNetwork, network) { return &gw } } return nil } func (a *WalletAPI) createWalletOnGateway(ctx context.Context, gateway discovery.GatewaySummary, req *connectorv1.OpenAccountRequest) (string, error) { var dialOpts []grpc.DialOption if a.insecure { dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) } else { dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) } conn, err := grpc.NewClient(gateway.InvokeURI, dialOpts...) if err != nil { return "", merrors.InternalWrap(err, "dial gateway") } defer conn.Close() client := connectorv1.NewConnectorServiceClient(conn) // Call with timeout callCtx, callCancel := context.WithTimeout(ctx, a.callTimeout) defer callCancel() resp, err := client.OpenAccount(callCtx, req) if err != nil { return "", err } if resp.GetError() != nil { return "", merrors.Internal(resp.GetError().GetMessage()) } account := resp.GetAccount() if account == nil || account.GetRef() == nil { return "", merrors.Internal("gateway returned empty account") } return strings.TrimSpace(account.GetRef().GetAccountId()), nil }