Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/go-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Go SDK

on:
push:
branches: [main]
paths:
- 'clients/go/**'
- '.github/workflows/go-sdk.yml'
pull_request:
paths:
- 'clients/go/**'
- '.github/workflows/go-sdk.yml'
workflow_dispatch:

defaults:
run:
working-directory: clients/go

jobs:
test:
name: Vet & test wraith-go
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Check formatting
run: test -z "$(gofmt -l .)"
- run: go vet ./...
- run: go test ./... -race -coverprofile=coverage.out
- name: Coverage summary
run: go tool cover -func=coverage.out | tail -1
2 changes: 2 additions & 0 deletions clients/go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
coverage.out
*.test
91 changes: 91 additions & 0 deletions clients/go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# wraith-go

Idiomatic Go client for the [Wraith](https://github.com/Miracle656/wraith)
Soroban token-transfer indexer REST API.

The client is split into **one package per resource** — `transfers`,
`accounts`, `assets`, `nfts`, `webhooks`, `status` — all sharing a small
transport in the root `wraith` package. Amounts are returned as `string` to
preserve full i128 precision.

## Install

```bash
go get github.com/Miracle656/wraith/clients/go
```

## Usage

```go
package main

import (
"context"
"fmt"
"log"

"github.com/Miracle656/wraith/clients/go/wraith"
"github.com/Miracle656/wraith/clients/go/transfers"
"github.com/Miracle656/wraith/clients/go/assets"
)

func main() {
c := wraith.New("https://wraith.example.com")
ctx := context.Background()

// Transfers received by an address.
tc := transfers.New(c)
page, err := tc.Incoming(ctx, "GABC...", &transfers.ListParams{
Limit: wraith.IntPtr(100),
})
if err != nil {
log.Fatal(err)
}
for _, t := range page.Transfers {
fmt.Println(t.ContractID, t.Amount, t.EventType)
}

// Most-active assets.
ac := assets.New(c)
pop, err := ac.PopularAssets(ctx, &assets.PopularParams{Window: "24h", By: "volume"})
if err != nil {
log.Fatal(err)
}
for _, a := range pop.Assets {
fmt.Println(a.ContractID, a.TransferCount, a.Volume)
}
}
```

A non-2xx response is returned as a `*wraith.APIError`:

```go
page, err := tc.Incoming(ctx, "GABC...", nil)
var apiErr *wraith.APIError
if errors.As(err, &apiErr) {
fmt.Println(apiErr.StatusCode, apiErr.Message)
}
```

## Packages

| Package | Constructor | List methods |
| ----------- | ----------------------- | --------------------------------------------- |
| `wraith` | `wraith.New(baseURL)` | shared transport, `APIError`, options |
| `transfers` | `transfers.New(c)` | `Incoming`, `Outgoing`, `ForAddress`, `ByTx` |
| `accounts` | `accounts.New(c)` | `Summary`, `Transfers` |
| `assets` | `assets.New(c)` | `PopularAssets` |
| `nfts` | `nfts.New(c)` | `Transfers` |
| `webhooks` | `webhooks.New(c)` | `List`, `Deliveries` |
| `status` | `status.New(c)` | `Get` |

Options: `wraith.WithHTTPClient(*http.Client)` and `wraith.WithHeader(k, v)`.

## Development

```bash
cd clients/go
go test ./... -cover
go vet ./...
gofmt -l .
```
57 changes: 57 additions & 0 deletions clients/go/accounts/accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Package accounts is the Wraith client for account-level endpoints.
package accounts

import (
"context"
"net/url"

"github.com/Miracle656/wraith/clients/go/transfers"
"github.com/Miracle656/wraith/clients/go/wraith"
)

// Client accesses the /accounts endpoints.
type Client struct {
c *wraith.Client
}

// New returns an accounts client backed by a shared *wraith.Client.
func New(c *wraith.Client) *Client { return &Client{c: c} }

// AssetHolding is one row of an account's per-asset summary. Amounts are kept
// as strings to preserve full i128 precision.
type AssetHolding struct {
ContractID string `json:"contractId"`
TotalSent string `json:"totalSent"`
TotalReceived string `json:"totalReceived"`
Net string `json:"net"`
TxCount int `json:"txCount"`
LastActivityAt *string `json:"lastActivityAt"`
DisplayTotalSent string `json:"displayTotalSent,omitempty"`
DisplayTotalReceived string `json:"displayTotalReceived,omitempty"`
DisplayNet string `json:"displayNet,omitempty"`
}

// Summary is an account's holdings across every asset it has touched.
type Summary struct {
Address string `json:"address"`
Assets []AssetHolding `json:"assets"`
}

// Summary returns an account's per-asset holdings (GET /accounts/{address}/summary).
func (a *Client) Summary(ctx context.Context, address string) (*Summary, error) {
var summary Summary
if err := a.c.Get(ctx, "/accounts/"+url.PathEscape(address)+"/summary", nil, &summary); err != nil {
return nil, err
}
return &summary, nil
}

// Transfers returns an account's transfers (GET /accounts/{address}/transfers).
// It reuses the shared transfers.ListParams filters and transfers.Page shape.
func (a *Client) Transfers(ctx context.Context, address string, params *transfers.ListParams) (*transfers.Page, error) {
var page transfers.Page
if err := a.c.Get(ctx, "/accounts/"+url.PathEscape(address)+"/transfers", params.Query(), &page); err != nil {
return nil, err
}
return &page, nil
}
58 changes: 58 additions & 0 deletions clients/go/accounts/accounts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package accounts_test

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/Miracle656/wraith/clients/go/accounts"
"github.com/Miracle656/wraith/clients/go/transfers"
"github.com/Miracle656/wraith/clients/go/wraith"
)

func TestSummary(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/accounts/GABC/summary" {
t.Errorf("path = %q", r.URL.Path)
}
w.Header().Set("content-type", "application/json")
_, _ = w.Write([]byte(`{"address":"GABC","assets":[{"contractId":"CABC","totalSent":"10","totalReceived":"30","net":"20","txCount":3}]}`))
}))
defer srv.Close()

c := accounts.New(wraith.New(srv.URL))
sum, err := c.Summary(context.Background(), "GABC")
if err != nil {
t.Fatalf("Summary: %v", err)
}
if sum.Address != "GABC" || len(sum.Assets) != 1 {
t.Fatalf("unexpected summary: %+v", sum)
}
if sum.Assets[0].Net != "20" || sum.Assets[0].TxCount != 3 {
t.Errorf("unexpected holding: %+v", sum.Assets[0])
}
}

func TestTransfers(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/accounts/GABC/transfers" {
t.Errorf("path = %q", r.URL.Path)
}
if r.URL.Query().Get("offset") != "0" {
t.Errorf("offset = %q, want 0", r.URL.Query().Get("offset"))
}
w.Header().Set("content-type", "application/json")
_, _ = w.Write([]byte(`{"total":0,"limit":50,"offset":0,"nextCursor":null,"transfers":[]}`))
}))
defer srv.Close()

c := accounts.New(wraith.New(srv.URL))
page, err := c.Transfers(context.Background(), "GABC", &transfers.ListParams{Offset: wraith.IntPtr(0)})
if err != nil {
t.Fatalf("Transfers: %v", err)
}
if page.Limit != 50 {
t.Errorf("limit = %d, want 50", page.Limit)
}
}
65 changes: 65 additions & 0 deletions clients/go/assets/assets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Package assets is the Wraith client for asset-level endpoints.
package assets

import (
"context"
"net/url"

"github.com/Miracle656/wraith/clients/go/wraith"
)

// Client accesses the /assets endpoints.
type Client struct {
c *wraith.Client
}

// New returns an assets client backed by a shared *wraith.Client.
func New(c *wraith.Client) *Client { return &Client{c: c} }

// PopularAsset is one asset on the /assets/popular leaderboard. Volume is kept
// as a string to preserve full i128 precision.
type PopularAsset struct {
ContractID string `json:"contractId"`
TransferCount int `json:"transferCount"`
Volume string `json:"volume"`
DisplayVolume string `json:"displayVolume,omitempty"`
}

// Popular is the /assets/popular response: a ranked list plus the query window.
type Popular struct {
Window string `json:"window"`
By string `json:"by"`
Assets []PopularAsset `json:"assets"`
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}

// PopularParams are the optional filters for PopularAssets.
type PopularParams struct {
Window string // e.g. "24h", "7d"
By string // e.g. "volume", "count"
Limit *int
Offset *int
}

func (p *PopularParams) query() url.Values {
q := url.Values{}
if p == nil {
return q
}
wraith.AddString(q, "window", p.Window)
wraith.AddString(q, "by", p.By)
wraith.AddInt(q, "limit", p.Limit)
wraith.AddInt(q, "offset", p.Offset)
return q
}

// PopularAssets returns the most-active assets (GET /assets/popular).
func (a *Client) PopularAssets(ctx context.Context, params *PopularParams) (*Popular, error) {
var popular Popular
if err := a.c.Get(ctx, "/assets/popular", params.query(), &popular); err != nil {
return nil, err
}
return &popular, nil
}
43 changes: 43 additions & 0 deletions clients/go/assets/assets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package assets_test

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/Miracle656/wraith/clients/go/assets"
"github.com/Miracle656/wraith/clients/go/wraith"
)

func TestPopularAssets(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/assets/popular" {
t.Errorf("path = %q", r.URL.Path)
}
q := r.URL.Query()
if q.Get("window") != "24h" || q.Get("by") != "volume" {
t.Errorf("query = %v", q)
}
w.Header().Set("content-type", "application/json")
_, _ = w.Write([]byte(`{"window":"24h","by":"volume","total":1,"limit":10,"offset":0,
"assets":[{"contractId":"CABC","transferCount":5,"volume":"100","displayVolume":"1.0"}]}`))
}))
defer srv.Close()

c := assets.New(wraith.New(srv.URL))
pop, err := c.PopularAssets(context.Background(), &assets.PopularParams{
Window: "24h",
By: "volume",
Limit: wraith.IntPtr(10),
})
if err != nil {
t.Fatalf("PopularAssets: %v", err)
}
if pop.Window != "24h" || len(pop.Assets) != 1 {
t.Fatalf("unexpected response: %+v", pop)
}
if pop.Assets[0].ContractID != "CABC" || pop.Assets[0].TransferCount != 5 {
t.Errorf("unexpected asset: %+v", pop.Assets[0])
}
}
3 changes: 3 additions & 0 deletions clients/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/Miracle656/wraith/clients/go

go 1.22
Loading
Loading