Files
treasury/cmd/treasuryd/main.go
zeekay fd60ac291a fix(provider): deregister marqeta/plaid lying stubs (honest Capabilities + doc.go)
Both adapters self-registered when their env vars were set, then returned
ErrNotSupported on every call. Routing logic could not distinguish a stub
from a real adapter — dangerous for payment routing.

Per yesterday's audit:
- Capabilities() now returns empty PaymentTypes/Currencies/Features/
  Countries and Status="placeholder" on both. Routers must see them
  as supporting nothing.
- Every method returns wrapped ErrNotSupported with explicit
  "<provider> adapter not implemented (<method>) — see doc.go" so
  the error trail is honest.
- doc.go starts with the required placeholder line pointing at the
  provider's official docs URL.
- cmd/treasuryd/main.go no longer registers either provider; instead
  it logs a warning when the env var is set, pointing at the doc.go
  TODO. Env vars stay in the docs so a future real implementation
  drops in unchanged. Imports kept under blank-import to preserve the
  stable package path.
2026-05-24 18:58:34 -07:00

230 lines
7.5 KiB
Go

package main
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/luxfi/treasury/pkg/api"
"github.com/luxfi/treasury/pkg/ledger"
"github.com/luxfi/treasury/pkg/provider"
"github.com/luxfi/treasury/pkg/provider/bridge"
"github.com/luxfi/treasury/pkg/provider/column"
"github.com/luxfi/treasury/pkg/provider/currencycloud"
"github.com/luxfi/treasury/pkg/provider/increase"
"github.com/luxfi/treasury/pkg/provider/lithic"
_ "github.com/luxfi/treasury/pkg/provider/marqeta" // placeholder — see pkg/provider/marqeta/doc.go
"github.com/luxfi/treasury/pkg/provider/mercury"
"github.com/luxfi/treasury/pkg/provider/moderntreasury"
"github.com/luxfi/treasury/pkg/provider/mpesa"
_ "github.com/luxfi/treasury/pkg/provider/plaid" // placeholder — see pkg/provider/plaid/doc.go
"github.com/luxfi/treasury/pkg/provider/square"
"github.com/luxfi/treasury/pkg/provider/stripe"
"github.com/luxfi/treasury/pkg/provider/unit"
"github.com/luxfi/treasury/pkg/provider/wise"
)
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
listenAddr := envOr("TREASURY_LISTEN", ":8091")
registry := provider.NewRegistry()
// CurrencyCloud — FX + cross-border payments
if login := os.Getenv("CURRENCYCLOUD_LOGIN_ID"); login != "" {
registry.Register(currencycloud.New(currencycloud.Config{
BaseURL: envOr("CURRENCYCLOUD_BASE_URL", currencycloud.DemoURL),
LoginID: login,
APIKey: os.Getenv("CURRENCYCLOUD_API_KEY"),
}))
log.Info().Msg("CurrencyCloud provider registered")
}
// Plaid — placeholder adapter; not production-ready. Skip registration
// even if PLAID_CLIENT_ID is set so the router never sees a lying stub.
if clientID := os.Getenv("PLAID_CLIENT_ID"); clientID != "" {
log.Warn().
Str("todo", "see github.com/luxfi/treasury/pkg/provider/plaid/doc.go").
Msg("plaid adapter is a placeholder, skipping registration")
}
// Modern Treasury — payment operations + ledger
if orgID := os.Getenv("MODERN_TREASURY_ORG_ID"); orgID != "" {
registry.Register(moderntreasury.New(moderntreasury.Config{
BaseURL: envOr("MODERN_TREASURY_BASE_URL", moderntreasury.ProdURL),
OrgID: orgID,
APIKey: os.Getenv("MODERN_TREASURY_API_KEY"),
}))
log.Info().Msg("Modern Treasury provider registered")
}
// Stripe Treasury — BaaS
if key := os.Getenv("STRIPE_SECRET_KEY"); key != "" {
registry.Register(stripe.New(stripe.Config{
BaseURL: envOr("STRIPE_BASE_URL", stripe.ProdURL),
SecretKey: key,
}))
log.Info().Msg("Stripe Treasury provider registered")
}
// Column — direct bank access (Fed member)
if key := os.Getenv("COLUMN_API_KEY"); key != "" {
registry.Register(column.New(column.Config{
BaseURL: envOr("COLUMN_BASE_URL", column.SandboxURL),
APIKey: key,
}))
log.Info().Msg("Column provider registered")
}
// Wise — international transfers + FX
if token := os.Getenv("WISE_API_TOKEN"); token != "" {
registry.Register(wise.New(wise.Config{
BaseURL: envOr("WISE_BASE_URL", wise.SandboxURL),
APIToken: token,
ProfileID: os.Getenv("WISE_PROFILE_ID"),
}))
log.Info().Msg("Wise provider registered")
}
// Marqeta — placeholder adapter; not production-ready. Skip registration
// even if MARQETA_APP_TOKEN is set so the router never sees a lying stub.
if os.Getenv("MARQETA_APP_TOKEN") != "" {
log.Warn().
Str("todo", "see github.com/luxfi/treasury/pkg/provider/marqeta/doc.go").
Msg("marqeta adapter is a placeholder, skipping registration")
}
// Bridge (by Stripe) — stablecoin orchestration
if key := os.Getenv("BRIDGE_API_KEY"); key != "" {
registry.Register(bridge.New(bridge.Config{
BaseURL: envOr("BRIDGE_BASE_URL", bridge.ProdURL),
APIKey: key,
}))
log.Info().Msg("Bridge provider registered")
}
// Unit — BaaS (white-label banking)
if token := os.Getenv("UNIT_TOKEN"); token != "" {
registry.Register(unit.New(unit.Config{
BaseURL: envOr("UNIT_BASE_URL", unit.SandboxURL),
Token: token,
}))
log.Info().Msg("Unit provider registered")
}
// Mercury — startup neobank
if key := os.Getenv("MERCURY_API_KEY"); key != "" {
registry.Register(mercury.New(mercury.Config{
BaseURL: envOr("MERCURY_BASE_URL", mercury.ProdURL),
APIKey: key,
}))
log.Info().Msg("Mercury provider registered")
}
// Square — payments + commerce
if token := os.Getenv("SQUARE_ACCESS_TOKEN"); token != "" {
registry.Register(square.New(square.Config{
BaseURL: envOr("SQUARE_BASE_URL", square.ProdURL),
AccessToken: token,
}))
log.Info().Msg("Square provider registered")
}
// Increase — banking infrastructure (ACH, wire, RTP, checks)
if key := os.Getenv("INCREASE_API_KEY"); key != "" {
registry.Register(increase.New(increase.Config{
BaseURL: envOr("INCREASE_BASE_URL", increase.SandboxURL),
APIKey: key,
}))
log.Info().Msg("Increase provider registered")
}
// M-Pesa — Safaricom Daraja (KE mobile money: B2C, C2B STK Push, B2B)
if key := os.Getenv("MPESA_CONSUMER_KEY"); key != "" {
baseURL := envOr("MPESA_BASE_URL", mpesa.SandboxURL)
if env := os.Getenv("MPESA_ENVIRONMENT"); env == "production" {
baseURL = envOr("MPESA_BASE_URL", mpesa.ProdURL)
}
registry.Register(mpesa.New(mpesa.Config{
BaseURL: baseURL,
ConsumerKey: key,
ConsumerSecret: os.Getenv("MPESA_CONSUMER_SECRET"),
Shortcode: os.Getenv("MPESA_SHORTCODE"),
InitiatorName: os.Getenv("MPESA_INITIATOR_NAME"),
InitiatorPassword: os.Getenv("MPESA_INITIATOR_PASSWORD"),
Passkey: os.Getenv("MPESA_PASSKEY"),
CallbackURL: os.Getenv("MPESA_CALLBACK_URL"),
QueueTimeoutURL: os.Getenv("MPESA_QUEUE_TIMEOUT_URL"),
ResultURL: os.Getenv("MPESA_RESULT_URL"),
Environment: envOr("MPESA_ENVIRONMENT", "sandbox"),
}))
log.Info().Msg("M-Pesa provider registered")
}
// Lithic — card issuing (virtual + physical)
if key := os.Getenv("LITHIC_API_KEY"); key != "" {
registry.Register(lithic.New(lithic.Config{
BaseURL: envOr("LITHIC_BASE_URL", lithic.SandboxURL),
APIKey: key,
}))
log.Info().Msg("Lithic provider registered")
}
if len(registry.List()) == 0 {
log.Warn().Msg("No treasury providers configured. Set env vars to enable providers.")
}
// --- Ledger (Postgres or in-memory) ---
var ledgerStore ledger.Store
if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" {
pgStore, err := ledger.NewPgStore(dbURL)
if err != nil {
log.Fatal().Err(err).Msg("Failed to connect to database")
}
if err := pgStore.Migrate(context.Background()); err != nil {
log.Fatal().Err(err).Msg("Failed to run ledger migrations")
}
ledgerStore = pgStore
log.Info().Msg("Ledger: Postgres-backed")
} else {
ledgerStore = ledger.NewMemStore()
log.Warn().Msg("Ledger: in-memory (set DATABASE_URL for persistence)")
}
ledgerSvc := ledger.NewService(ledgerStore)
srv := api.NewServer(registry, ledgerSvc, listenAddr)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
errCh := make(chan error, 1)
go func() {
log.Info().Str("addr", listenAddr).Strs("providers", registry.List()).Msg("Treasury API starting")
errCh <- srv.Start()
}()
select {
case <-ctx.Done():
log.Info().Msg("Shutting down...")
if err := srv.Shutdown(context.Background()); err != nil {
log.Error().Err(err).Msg("Shutdown error")
}
case err := <-errCh:
if err != nil {
log.Fatal().Err(err).Msg("Server failed")
}
}
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}