package sresponse import ( "fmt" "net/http" "strings" "time" "github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/mlogger" paymenttypes "github.com/tech/sendico/pkg/payments/types" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" "google.golang.org/protobuf/types/known/timestamppb" ) type walletAsset struct { Chain string `json:"chain"` TokenSymbol string `json:"tokenSymbol"` ContractAddress string `json:"contractAddress"` } type wallet struct { WalletRef string `json:"walletRef"` OrganizationRef string `json:"organizationRef"` OwnerRef string `json:"ownerRef"` Asset walletAsset `json:"asset"` DepositAddress string `json:"depositAddress"` Status string `json:"status"` Metadata map[string]string `json:"metadata,omitempty"` Name string `json:"name"` Description *string `json:"description,omitempty"` CreatedAt string `json:"createdAt,omitempty"` UpdatedAt string `json:"updatedAt,omitempty"` } type walletsResponse struct { authResponse `json:",inline"` Wallets []wallet `json:"wallets"` Page *paginationv1.CursorPageResponse `json:"page,omitempty"` } type walletBalance struct { Available *paymenttypes.Money `json:"available,omitempty"` PendingInbound *paymenttypes.Money `json:"pendingInbound,omitempty"` PendingOutbound *paymenttypes.Money `json:"pendingOutbound,omitempty"` CalculatedAt string `json:"calculatedAt,omitempty"` } type walletBalanceResponse struct { authResponse `json:",inline"` Balance walletBalance `json:"balance"` } func Wallets(logger mlogger.Logger, resp *chainv1.ListManagedWalletsResponse, accessToken *TokenData) http.HandlerFunc { dto := walletsResponse{ Page: resp.GetPage(), authResponse: authResponse{AccessToken: *accessToken}, } dto.Wallets = make([]wallet, 0, len(resp.GetWallets())) for _, w := range resp.GetWallets() { dto.Wallets = append(dto.Wallets, toWallet(w)) } return response.Ok(logger, dto) } func WalletBalance(logger mlogger.Logger, bal *chainv1.WalletBalance, accessToken *TokenData) http.HandlerFunc { return response.Ok(logger, walletBalanceResponse{ Balance: toWalletBalance(bal), authResponse: authResponse{AccessToken: *accessToken}, }) } func toWallet(w *chainv1.ManagedWallet) wallet { if w == nil { return wallet{} } asset := w.GetAsset() chain := "" token := "" contract := "" if asset != nil { chain = chainNetworkValue(asset.GetChain()) token = asset.GetTokenSymbol() contract = asset.GetContractAddress() } name := "" if d := w.GetDescribable(); d != nil { name = strings.TrimSpace(d.GetName()) } if name == "" { name = strings.TrimSpace(w.GetMetadata()["name"]) } if name == "" { name = w.GetWalletRef() } var description *string if d := w.GetDescribable(); d != nil && d.Description != nil { if trimmed := strings.TrimSpace(d.GetDescription()); trimmed != "" { description = &trimmed } } if description == nil { if trimmed := strings.TrimSpace(w.GetMetadata()["description"]); trimmed != "" { description = &trimmed } } return wallet{ WalletRef: w.GetWalletRef(), OrganizationRef: w.GetOrganizationRef(), OwnerRef: w.GetOwnerRef(), Asset: walletAsset{ Chain: chain, TokenSymbol: token, ContractAddress: contract, }, DepositAddress: w.GetDepositAddress(), Status: w.GetStatus().String(), Metadata: w.GetMetadata(), Name: name, Description: description, CreatedAt: tsToString(w.GetCreatedAt()), UpdatedAt: tsToString(w.GetUpdatedAt()), } } func toWalletBalance(b *chainv1.WalletBalance) walletBalance { if b == nil { return walletBalance{} } return walletBalance{ Available: toMoney(b.GetAvailable()), PendingInbound: toMoney(b.GetPendingInbound()), PendingOutbound: toMoney(b.GetPendingOutbound()), CalculatedAt: tsToString(b.GetCalculatedAt()), } } func tsToString(ts *timestamppb.Timestamp) string { if ts == nil { return "" } return ts.AsTime().UTC().Format(time.RFC3339) } func chainNetworkValue(chain chainv1.ChainNetwork) string { name := chain.String() if !strings.HasPrefix(name, "CHAIN_NETWORK_") { return "unspecified" } trimmed := strings.TrimPrefix(name, "CHAIN_NETWORK_") if trimmed == "" { return "unspecified" } return strings.ToLower(trimmed) } // WalletsFromAccounts converts connector accounts to wallet response format. // Used when querying multiple gateways via discovery. func WalletsFromAccounts(logger mlogger.Logger, accounts []*connectorv1.Account, accessToken *TokenData) http.HandlerFunc { dto := walletsResponse{ authResponse: authResponse{AccessToken: *accessToken}, } dto.Wallets = make([]wallet, 0, len(accounts)) for _, acc := range accounts { if acc == nil { continue } dto.Wallets = append(dto.Wallets, accountToWallet(acc)) } return response.Ok(logger, dto) } func accountToWallet(acc *connectorv1.Account) wallet { if acc == nil { return wallet{} } // Extract wallet details from provider details details := map[string]interface{}{} if acc.GetProviderDetails() != nil { details = acc.GetProviderDetails().AsMap() } walletRef := "" if ref := acc.GetRef(); ref != nil { walletRef = strings.TrimSpace(ref.GetAccountId()) } if v := stringFromDetails(details, "wallet_ref"); v != "" { walletRef = v } organizationRef := stringFromDetails(details, "organization_ref") ownerRef := strings.TrimSpace(acc.GetOwnerRef()) if v := stringFromDetails(details, "owner_ref"); v != "" { ownerRef = v } chain := stringFromDetails(details, "network") tokenSymbol := stringFromDetails(details, "token_symbol") contractAddress := stringFromDetails(details, "contract_address") depositAddress := stringFromDetails(details, "deposit_address") name := "" if d := acc.GetDescribable(); d != nil { name = strings.TrimSpace(d.GetName()) } if name == "" { name = strings.TrimSpace(acc.GetLabel()) } if name == "" { name = walletRef } var description *string if d := acc.GetDescribable(); d != nil && d.Description != nil { if trimmed := strings.TrimSpace(d.GetDescription()); trimmed != "" { description = &trimmed } } status := acc.GetState().String() // Convert connector state to wallet status format switch acc.GetState() { case connectorv1.AccountState_ACCOUNT_ACTIVE: status = "MANAGED_WALLET_ACTIVE" case connectorv1.AccountState_ACCOUNT_SUSPENDED: status = "MANAGED_WALLET_SUSPENDED" case connectorv1.AccountState_ACCOUNT_CLOSED: status = "MANAGED_WALLET_CLOSED" } return wallet{ WalletRef: walletRef, OrganizationRef: organizationRef, OwnerRef: ownerRef, Asset: walletAsset{ Chain: chain, TokenSymbol: tokenSymbol, ContractAddress: contractAddress, }, DepositAddress: depositAddress, Status: status, Name: name, Description: description, CreatedAt: tsToString(acc.GetCreatedAt()), UpdatedAt: tsToString(acc.GetUpdatedAt()), } } func stringFromDetails(details map[string]interface{}, key string) string { if details == nil { return "" } if value, ok := details[key]; ok { return strings.TrimSpace(fmt.Sprint(value)) } return "" } // WalletBalanceFromConnector converts connector balance to wallet balance response format. // Used when querying gateways via discovery. func WalletBalanceFromConnector(logger mlogger.Logger, bal *connectorv1.Balance, accessToken *TokenData) http.HandlerFunc { return response.Ok(logger, walletBalanceResponse{ Balance: connectorBalanceToWalletBalance(bal), authResponse: authResponse{AccessToken: *accessToken}, }) } func connectorBalanceToWalletBalance(b *connectorv1.Balance) walletBalance { if b == nil { return walletBalance{} } return walletBalance{ Available: connectorMoneyToModel(b.GetAvailable()), PendingInbound: connectorMoneyToModel(b.GetPendingInbound()), PendingOutbound: connectorMoneyToModel(b.GetPendingOutbound()), CalculatedAt: tsToString(b.GetCalculatedAt()), } } func connectorMoneyToModel(m *moneyv1.Money) *paymenttypes.Money { if m == nil { return nil } return &paymenttypes.Money{ Amount: m.GetAmount(), Currency: m.GetCurrency(), } }