From df8d54237b99bc75879225462c870cf91d8edd25 Mon Sep 17 00:00:00 2001 From: Prad N Date: Fri, 30 May 2025 17:26:51 -0400 Subject: [PATCH] feat: introduce API client for market data --- cmd/radar/main.go | 7 +- cmd/worker/main.go | 7 +- cmd/worker/routes.go | 32 ------- {cmd/radar => config}/routes.go | 8 +- handlers/auth_handler.go | 2 +- handlers/page_handler.go | 2 +- internal/api/client.go | 35 +++++++ .../types.go => internal/api/market.go | 91 +++++++++++-------- internal/api/utils.go | 43 +++++++++ internal/jobs/events.go | 2 +- middleware/marketapi/middleware.go | 77 ---------------- middleware/session/middleware.go | 10 +- middleware/sonrapi/middleware.go | 42 --------- {middleware => pkg}/cookies/cookies.go | 0 {middleware => pkg}/headers/headers.go | 0 {middleware => pkg}/render/render.go | 0 16 files changed, 151 insertions(+), 207 deletions(-) delete mode 100644 cmd/worker/routes.go rename {cmd/radar => config}/routes.go (90%) create mode 100644 internal/api/client.go rename middleware/marketapi/types.go => internal/api/market.go (52%) create mode 100644 internal/api/utils.go delete mode 100644 middleware/marketapi/middleware.go delete mode 100644 middleware/sonrapi/middleware.go rename {middleware => pkg}/cookies/cookies.go (100%) rename {middleware => pkg}/headers/headers.go (100%) rename {middleware => pkg}/render/render.go (100%) diff --git a/cmd/radar/main.go b/cmd/radar/main.go index 30866fe..cd0a8a0 100644 --- a/cmd/radar/main.go +++ b/cmd/radar/main.go @@ -9,10 +9,10 @@ import ( "net/http" "github.com/labstack/echo/v4" + "github.com/sonr-io/motr/config" "github.com/sonr-io/motr/middleware/database" "github.com/sonr-io/motr/middleware/kvstore" "github.com/sonr-io/motr/middleware/session" - "github.com/sonr-io/motr/middleware/sonrapi" "github.com/sonr-io/motr/middleware/webauthn" "github.com/syumai/workers" "github.com/syumai/workers/cloudflare/cron" @@ -31,11 +31,10 @@ func loadHandler() http.Handler { session.Middleware(), database.Middleware(), kvstore.Middleware(), - sonrapi.Middleware(), webauthn.Middleware(), ) - setupViews(e) - setupPartials(e) + config.RegisterViews(e) + config.RegisterPartials(e) return e } diff --git a/cmd/worker/main.go b/cmd/worker/main.go index 30866fe..cd0a8a0 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -9,10 +9,10 @@ import ( "net/http" "github.com/labstack/echo/v4" + "github.com/sonr-io/motr/config" "github.com/sonr-io/motr/middleware/database" "github.com/sonr-io/motr/middleware/kvstore" "github.com/sonr-io/motr/middleware/session" - "github.com/sonr-io/motr/middleware/sonrapi" "github.com/sonr-io/motr/middleware/webauthn" "github.com/syumai/workers" "github.com/syumai/workers/cloudflare/cron" @@ -31,11 +31,10 @@ func loadHandler() http.Handler { session.Middleware(), database.Middleware(), kvstore.Middleware(), - sonrapi.Middleware(), webauthn.Middleware(), ) - setupViews(e) - setupPartials(e) + config.RegisterViews(e) + config.RegisterPartials(e) return e } diff --git a/cmd/worker/routes.go b/cmd/worker/routes.go deleted file mode 100644 index bb87be3..0000000 --- a/cmd/worker/routes.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build js && wasm -// +build js,wasm - -package main - -import ( - "github.com/labstack/echo/v4" - "github.com/sonr-io/motr/handlers" - "github.com/sonr-io/motr/internal/ui/home" - "github.com/sonr-io/motr/internal/ui/login" - "github.com/sonr-io/motr/internal/ui/register" - "github.com/sonr-io/motr/middleware/render" -) - -// ╭────────────────────────────────────────────────╮ -// │ HTTP Routes │ -// ╰────────────────────────────────────────────────╯ - -func setupViews(e *echo.Echo) { - e.GET("/", render.Page(home.HomeView())) - e.GET("/login", render.Page(login.LoginView())) - e.GET("/register", render.Page(register.RegisterView())) -} - -func setupPartials(e *echo.Echo) { - e.POST("/login/:handle/check", handlers.HandleLoginCheck) - e.POST("/login/:handle/finish", handlers.HandleLoginFinish) - e.POST("/register/:handle", handlers.HandleRegisterStart) - e.POST("/register/:handle/check", handlers.HandleRegisterCheck) - e.POST("/register/:handle/finish", handlers.HandleRegisterFinish) - e.POST("/status", handlers.HandleStatusCheck) -} diff --git a/cmd/radar/routes.go b/config/routes.go similarity index 90% rename from cmd/radar/routes.go rename to config/routes.go index bb87be3..490e035 100644 --- a/cmd/radar/routes.go +++ b/config/routes.go @@ -1,7 +1,7 @@ //go:build js && wasm // +build js,wasm -package main +package config import ( "github.com/labstack/echo/v4" @@ -9,20 +9,20 @@ import ( "github.com/sonr-io/motr/internal/ui/home" "github.com/sonr-io/motr/internal/ui/login" "github.com/sonr-io/motr/internal/ui/register" - "github.com/sonr-io/motr/middleware/render" + "github.com/sonr-io/motr/pkg/render" ) // ╭────────────────────────────────────────────────╮ // │ HTTP Routes │ // ╰────────────────────────────────────────────────╯ -func setupViews(e *echo.Echo) { +func RegisterViews(e *echo.Echo) { e.GET("/", render.Page(home.HomeView())) e.GET("/login", render.Page(login.LoginView())) e.GET("/register", render.Page(register.RegisterView())) } -func setupPartials(e *echo.Echo) { +func RegisterPartials(e *echo.Echo) { e.POST("/login/:handle/check", handlers.HandleLoginCheck) e.POST("/login/:handle/finish", handlers.HandleLoginFinish) e.POST("/register/:handle", handlers.HandleRegisterStart) diff --git a/handlers/auth_handler.go b/handlers/auth_handler.go index 7244507..797c1d4 100644 --- a/handlers/auth_handler.go +++ b/handlers/auth_handler.go @@ -4,7 +4,7 @@ import ( "github.com/labstack/echo/v4" "github.com/sonr-io/motr/internal/ui/login" "github.com/sonr-io/motr/internal/ui/register" - "github.com/sonr-io/motr/middleware/render" + "github.com/sonr-io/motr/pkg/render" ) func HandleLoginCheck(c echo.Context) error { diff --git a/handlers/page_handler.go b/handlers/page_handler.go index c2e20e1..0d1ff4b 100644 --- a/handlers/page_handler.go +++ b/handlers/page_handler.go @@ -5,7 +5,7 @@ import ( "github.com/sonr-io/motr/internal/ui/home" "github.com/sonr-io/motr/internal/ui/login" "github.com/sonr-io/motr/internal/ui/register" - "github.com/sonr-io/motr/middleware/render" + "github.com/sonr-io/motr/pkg/render" ) func RenderHomePage(c echo.Context) error { diff --git a/internal/api/client.go b/internal/api/client.go new file mode 100644 index 0000000..8d7fa8f --- /dev/null +++ b/internal/api/client.go @@ -0,0 +1,35 @@ +//go:build js && wasm +// +build js,wasm + +package api + +import ( + "context" + + "github.com/syumai/workers/cloudflare/fetch" +) + +type Response interface { + UnmarshalJSON(data []byte) error +} + +type Client interface { + MarketAPI +} + +type client struct { + fc *fetch.Client + ctx context.Context + MarketAPI +} + +func NewClient(ctx context.Context) *client { + fc := fetch.NewClient() + c := &client{ + fc: fc, + ctx: ctx, + } + marketAPI := NewMarketAPI(c, ctx) + c.MarketAPI = marketAPI + return c +} diff --git a/middleware/marketapi/types.go b/internal/api/market.go similarity index 52% rename from middleware/marketapi/types.go rename to internal/api/market.go index 6d66d00..14c182c 100644 --- a/middleware/marketapi/types.go +++ b/internal/api/market.go @@ -1,18 +1,67 @@ //go:build js && wasm // +build js,wasm -package marketapi +package api import ( + "context" "encoding/json" "fmt" - "net/http" - - "github.com/syumai/workers/cloudflare/fetch" ) -type Response interface { - UnmarshalJSON(data []byte) error +const ( + kCryptoAPIURL = "https://api.alternative.me" + kCryptoAPIListings = "/v2/listings" + kCryptoAPITickers = "/v2/ticker" + kCryptoAPIGlobal = "/v2/global" +) + +type MarketAPI interface { + Listings(symbol string) (*ListingsResponse, error) + Ticker(symbol string) (*TickersResponse, error) + GlobalMarket() (*GlobalMarketResponse, error) +} + +type marketAPI struct { + client *client + ctx context.Context +} + +func NewMarketAPI(c *client, ctx context.Context) *marketAPI { + return &marketAPI{ + client: c, + ctx: ctx, + } +} + +func (m *marketAPI) Listings(symbol string) (*ListingsResponse, error) { + r := buildRequest(m.ctx, fmt.Sprintf("%s/%s", kCryptoAPIListings, symbol)) + v := &ListingsResponse{} + err := doFetch(m.client.fc, r, v) + if err != nil { + return nil, err + } + return v, nil +} + +func (m *marketAPI) Ticker(symbol string) (*TickersResponse, error) { + r := buildRequest(m.ctx, fmt.Sprintf("%s/%s", kCryptoAPITickers, symbol)) + v := &TickersResponse{} + err := doFetch(m.client.fc, r, v) + if err != nil { + return nil, err + } + return v, nil +} + +func (m *marketAPI) GlobalMarket() (*GlobalMarketResponse, error) { + r := buildRequest(m.ctx, kCryptoAPIGlobal) + v := &GlobalMarketResponse{} + err := doFetch(m.client.fc, r, v) + if err != nil { + return nil, err + } + return v, nil } type ListingsResponse struct { @@ -66,33 +115,3 @@ type GlobalMarketResponse struct { func (r *GlobalMarketResponse) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, r) } - -func (c *Context) buildRequest(url string) *fetch.Request { - r, err := fetch.NewRequest(c.Request().Context(), http.MethodGet, url, nil) - if err != nil { - fmt.Println(err) - return nil - } - r.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0") - return r -} - -func (c *Context) fetch(r *fetch.Request, v Response) error { - resp, err := c.client.Do(r, nil) - if err != nil { - return fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() // Ensure body is always closed - - // Check for non-200 status codes - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - // Directly decode JSON into the response struct - if err := json.NewDecoder(resp.Body).Decode(v); err != nil { - return fmt.Errorf("failed to decode response: %w", err) - } - - return nil -} diff --git a/internal/api/utils.go b/internal/api/utils.go new file mode 100644 index 0000000..088b003 --- /dev/null +++ b/internal/api/utils.go @@ -0,0 +1,43 @@ +//go:build js && wasm +// +build js,wasm + +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/syumai/workers/cloudflare/fetch" +) + +func buildRequest(c context.Context, url string) *fetch.Request { + r, err := fetch.NewRequest(c, http.MethodGet, url, nil) + if err != nil { + fmt.Println(err) + return nil + } + r.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0") + return r +} + +func doFetch(c *fetch.Client, r *fetch.Request, v Response) error { + resp, err := c.Do(r, nil) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() // Ensure body is always closed + + // Check for non-200 status codes + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + // Directly decode JSON into the response struct + if err := json.NewDecoder(resp.Body).Decode(v); err != nil { + return fmt.Errorf("failed to decode response: %w", err) + } + + return nil +} diff --git a/internal/jobs/events.go b/internal/jobs/events.go index fb9b182..f80b145 100644 --- a/internal/jobs/events.go +++ b/internal/jobs/events.go @@ -4,7 +4,7 @@ type EventTrigger string var ( EventTriggerMinute = EventTrigger("0 * * * * *") // Every minute (with seconds) - EventTriggerHourly = EventTrigger("0 0 * * * *") // Every hour at minute 0 + EventTriggerHourly = EventTrigger("0 */1 * * *") // Every hour at minute 0 EventTriggerDaily = EventTrigger("0 0 0 * * *") // Every day at 00:00:00 EventTriggerWeekly = EventTrigger("0 0 0 * * 0") // Every Sunday at 00:00:00 EventTriggerMonthly = EventTrigger("0 0 0 1 * *") // First day of every month at 00:00:00 diff --git a/middleware/marketapi/middleware.go b/middleware/marketapi/middleware.go deleted file mode 100644 index 37ba56e..0000000 --- a/middleware/marketapi/middleware.go +++ /dev/null @@ -1,77 +0,0 @@ -//go:build js && wasm -// +build js,wasm - -package marketapi - -import ( - "errors" - "fmt" - - "github.com/labstack/echo/v4" - "github.com/syumai/workers/cloudflare/fetch" -) - -const ( - kCryptoAPIURL = "https://api.alternative.me" - kCryptoAPIListings = "/v2/listings" - kCryptoAPITickers = "/v2/ticker" - kCryptoAPIGlobal = "/v2/global" -) - -type Context struct { - echo.Context - client *fetch.Client -} - -func (c *Context) Listings(symbol string) (*ListingsResponse, error) { - r := c.buildRequest(fmt.Sprintf("%s/%s", kCryptoAPIListings, symbol)) - v := &ListingsResponse{} - err := c.fetch(r, v) - if err != nil { - return nil, err - } - return v, nil -} - -func (c *Context) Ticker(symbol string) (*TickersResponse, error) { - r := c.buildRequest(fmt.Sprintf("%s/%s", kCryptoAPITickers, symbol)) - v := &TickersResponse{} - err := c.fetch(r, v) - if err != nil { - return nil, err - } - return v, nil -} - -func (c *Context) GlobalMarket() (*GlobalMarketResponse, error) { - r := c.buildRequest(kCryptoAPIGlobal) - v := &GlobalMarketResponse{} - err := c.fetch(r, v) - if err != nil { - return nil, err - } - return v, nil -} - -// Middleware is a middleware that adds a new key to the context -func Middleware() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - f := fetch.NewClient() - return func(c echo.Context) error { - ctx := &Context{ - Context: c, - client: f, - } - return next(ctx) - } - } -} - -// Unwrap unwraps the session context -func Unwrap(c echo.Context) (*Context, error) { - cc := c.(*Context) - if cc == nil { - return nil, errors.New("failed to unwrap session context") - } - return cc, nil -} diff --git a/middleware/session/middleware.go b/middleware/session/middleware.go index fecd9a7..9920db2 100644 --- a/middleware/session/middleware.go +++ b/middleware/session/middleware.go @@ -6,11 +6,11 @@ package session import ( "github.com/labstack/echo/v4" "github.com/segmentio/ksuid" - "github.com/sonr-io/motr/middleware/cookies" + "github.com/sonr-io/motr/pkg/cookies" ) // Context is a session context -type SessionContext struct { +type Context struct { echo.Context ID string `json:"id"` } @@ -19,7 +19,7 @@ type SessionContext struct { func Middleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - ctx := &SessionContext{ + ctx := &Context{ Context: c, ID: DetermineID(c), } @@ -29,8 +29,8 @@ func Middleware() echo.MiddlewareFunc { } // Unwrap unwraps the session context -func Unwrap(c echo.Context) *SessionContext { - cc := c.(*SessionContext) +func Unwrap(c echo.Context) *Context { + cc := c.(*Context) if cc == nil { panic("failed to unwrap session context") } diff --git a/middleware/sonrapi/middleware.go b/middleware/sonrapi/middleware.go deleted file mode 100644 index 5e9e653..0000000 --- a/middleware/sonrapi/middleware.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build js && wasm -// +build js,wasm - -package sonrapi - -import ( - "errors" - - "github.com/labstack/echo/v4" -) - -const ( - kCryptoAPIURL = "https://api.alternative.me" - kCryptoAPIListings = "/v2/listings" - kCryptoAPITickers = "/v2/ticker" - kCryptoAPIGlobal = "/v2/global" -) - -type Context struct { - echo.Context -} - -// Middleware is a middleware that adds a new key to the context -func Middleware() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - ctx := &Context{ - Context: c, - } - return next(ctx) - } - } -} - -// Unwrap unwraps the session context -func Unwrap(c echo.Context) (*Context, error) { - cc := c.(*Context) - if cc == nil { - return nil, errors.New("failed to unwrap session context") - } - return cc, nil -} diff --git a/middleware/cookies/cookies.go b/pkg/cookies/cookies.go similarity index 100% rename from middleware/cookies/cookies.go rename to pkg/cookies/cookies.go diff --git a/middleware/headers/headers.go b/pkg/headers/headers.go similarity index 100% rename from middleware/headers/headers.go rename to pkg/headers/headers.go diff --git a/middleware/render/render.go b/pkg/render/render.go similarity index 100% rename from middleware/render/render.go rename to pkg/render/render.go