feat: introduce API client for market data

This commit is contained in:
2025-05-30 17:26:51 -04:00
parent 1d379362f3
commit df8d54237b
16 changed files with 151 additions and 207 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 {

35
internal/api/client.go Normal file
View File

@@ -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
}

View File

@@ -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
}

43
internal/api/utils.go Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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
}