Skip to content

Commit b085958

Browse files
Quantumlyyclaude
andcommitted
Add Sparkscan CLI with full command suite, tests, and release pipeline
Implements a CLI wrapping sparkscan-api-go for querying addresses, tokens, transactions, and network stats on the Spark network. Includes mock-based tests, goreleaser config, CI/CD workflows, and a curl-pipe installer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6d1215d commit b085958

50 files changed

Lines changed: 2993 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# MIT License
2+
#
3+
# Copyright (c) 2026 Dune Analytics
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
name: CI
24+
25+
on:
26+
push:
27+
pull_request:
28+
29+
jobs:
30+
test:
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@v4
34+
35+
- uses: actions/setup-go@v5
36+
with:
37+
go-version-file: go.mod
38+
39+
- run: go test -race ./...

.github/workflows/release.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# MIT License
2+
#
3+
# Copyright (c) 2026 Dune Analytics
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
name: Release
24+
25+
on:
26+
push:
27+
tags:
28+
- "v*"
29+
30+
permissions:
31+
contents: write
32+
33+
jobs:
34+
release:
35+
runs-on: ubuntu-latest
36+
steps:
37+
- name: Checkout
38+
uses: actions/checkout@v4
39+
with:
40+
fetch-depth: 0
41+
42+
- name: Set up Go
43+
uses: actions/setup-go@v5
44+
with:
45+
go-version-file: go.mod
46+
47+
- name: Run GoReleaser
48+
uses: goreleaser/goreleaser-action@v6
49+
with:
50+
distribution: goreleaser
51+
version: "~> v2"
52+
args: release --clean
53+
env:
54+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Binaries
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
*.test
8+
*.out
9+
10+
# Local tools
11+
/bin/
12+
13+
# CLI binary
14+
sparkscan-cli
15+
16+
# IDE
17+
.idea/
18+
.vscode/
19+
*.swp
20+
21+
# Environment
22+
.env
23+
24+
# Conductor
25+
.context/

.golangci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
linters:
3+
enable:
4+
- goimports
5+
- stylecheck
6+
- lll
7+
disable:
8+
- errcheck
9+
10+
run:
11+
go: '1.25'
12+
13+
issues:
14+
exclude-rules:
15+
- linters:
16+
- lll
17+
source: "// nolint:lll"

.goreleaser.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
version: 2
2+
3+
project_name: sparkscan-cli
4+
5+
before:
6+
hooks:
7+
- go mod tidy
8+
9+
builds:
10+
- main: ./cmd
11+
binary: sparkscan
12+
env:
13+
- CGO_ENABLED=0
14+
ldflags:
15+
- -s -w
16+
- -X main.version={{.Version}}
17+
- -X main.commit={{.Commit}}
18+
- -X main.date={{.Date}}
19+
goos:
20+
- linux
21+
- darwin
22+
- windows
23+
goarch:
24+
- amd64
25+
- arm64
26+
27+
archives:
28+
- formats:
29+
- tar.gz
30+
name_template: >-
31+
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}
32+
format_overrides:
33+
- goos: windows
34+
formats:
35+
- zip
36+
37+
checksum:
38+
name_template: "checksums.txt"
39+
40+
changelog:
41+
sort: asc
42+
filters:
43+
exclude:
44+
- "^docs:"
45+
- "^test:"
46+
- "^ci:"
47+
- "^chore:"
48+
49+
release:
50+
github:
51+
owner: refrakts
52+
name: sparkscan-cli
53+
draft: false
54+
prerelease: auto
55+
extra_files:
56+
- glob: install.sh

Makefile

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# MIT License
2+
#
3+
# Copyright (c) 2026 Dune Analytics
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
.PHONY: all build test lint clean
24+
25+
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null | sed 's/^v//')
26+
COMMIT ?= $(shell git rev-parse --short=12 HEAD 2>/dev/null || echo "unknown")
27+
DATE ?= $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
28+
LDFLAGS = -s -w \
29+
-X main.version=$(VERSION) \
30+
-X main.commit=$(COMMIT) \
31+
-X main.date=$(DATE)
32+
33+
all: lint test build
34+
35+
build:
36+
go build -ldflags '$(LDFLAGS)' -o bin/sparkscan ./cmd
37+
38+
test:
39+
go test -timeout=30s -race -cover ./...
40+
41+
lint: bin/golangci-lint
42+
go fmt ./...
43+
go vet ./...
44+
bin/golangci-lint -c .golangci.yml run ./...
45+
go mod tidy
46+
47+
run:
48+
go run -ldflags '$(LDFLAGS)' ./cmd $(ARGS)
49+
50+
bin:
51+
mkdir -p bin
52+
53+
bin/golangci-lint: bin
54+
GOBIN=$(PWD)/bin go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8
55+
56+
clean:
57+
rm -rf bin/

cli/root.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
sparkscan "github.com/refrakts/sparkscan-api-go"
8+
"github.com/spf13/cobra"
9+
10+
"github.com/refrakts/sparkscan-cli/cmd/address"
11+
"github.com/refrakts/sparkscan-cli/cmd/stats"
12+
"github.com/refrakts/sparkscan-cli/cmd/token"
13+
"github.com/refrakts/sparkscan-cli/cmd/tx"
14+
"github.com/refrakts/sparkscan-cli/cmd/version"
15+
"github.com/refrakts/sparkscan-cli/cmdutil"
16+
"github.com/refrakts/sparkscan-cli/output"
17+
)
18+
19+
// NewRootCmd creates the root sparkscan command.
20+
func NewRootCmd(versionStr string) *cobra.Command {
21+
root := &cobra.Command{
22+
Use: "sparkscan",
23+
Short: "CLI for the Sparkscan API",
24+
Long: "A command-line interface for the Sparkscan blockchain explorer API.\n\nQuery addresses, tokens, transactions, and network statistics on the Spark network.",
25+
SilenceUsage: true,
26+
SilenceErrors: true,
27+
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
28+
if cmd.Annotations != nil && cmd.Annotations["skipClient"] == "true" {
29+
return nil
30+
}
31+
32+
apiKey, _ := cmd.Flags().GetString("api-key")
33+
if apiKey == "" {
34+
apiKey = os.Getenv("SPARKSCAN_API_KEY")
35+
}
36+
37+
baseURL, _ := cmd.Flags().GetString("base-url")
38+
39+
var opts []sparkscan.Option
40+
if apiKey != "" {
41+
opts = append(opts, sparkscan.WithAPIKey(apiKey))
42+
}
43+
44+
client, err := sparkscan.NewClient(baseURL, opts...)
45+
if err != nil {
46+
return fmt.Errorf("creating client: %w", err)
47+
}
48+
49+
cmdutil.SetClient(cmd, client)
50+
return nil
51+
},
52+
}
53+
54+
root.PersistentFlags().String("api-key", "", "Sparkscan API key (overrides SPARKSCAN_API_KEY)")
55+
root.PersistentFlags().String("base-url", "https://api.sparkscan.io", "API base URL")
56+
root.PersistentFlags().String("network", "MAINNET", "Network: MAINNET or REGTEST")
57+
output.AddFormatFlag(root)
58+
59+
root.AddCommand(address.NewCmd())
60+
root.AddCommand(token.NewCmd())
61+
root.AddCommand(tx.NewCmd())
62+
root.AddCommand(stats.NewCmd())
63+
root.AddCommand(version.NewCmd(versionStr))
64+
65+
return root
66+
}
67+
68+
// Execute runs the CLI.
69+
func Execute(version, commit, date string) {
70+
versionStr := fmt.Sprintf("%s (commit: %s, built: %s)", version, commit, date)
71+
root := NewRootCmd(versionStr)
72+
73+
if err := root.Execute(); err != nil {
74+
fmt.Fprintln(os.Stderr, err)
75+
os.Exit(1)
76+
}
77+
}

cmd/address/address.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package address
2+
3+
import "github.com/spf13/cobra"
4+
5+
// NewCmd returns the address parent command.
6+
func NewCmd() *cobra.Command {
7+
cmd := &cobra.Command{
8+
Use: "address",
9+
Short: "Query Spark addresses",
10+
}
11+
cmd.AddCommand(newGetCmd())
12+
cmd.AddCommand(newTokensCmd())
13+
cmd.AddCommand(newTransactionsCmd())
14+
cmd.AddCommand(newHistoryCmd())
15+
return cmd
16+
}

cmd/address/get.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package address
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/refrakts/sparkscan-api-go/ogen"
8+
"github.com/spf13/cobra"
9+
10+
"github.com/refrakts/sparkscan-cli/cmdutil"
11+
"github.com/refrakts/sparkscan-cli/output"
12+
)
13+
14+
func newGetCmd() *cobra.Command {
15+
return &cobra.Command{
16+
Use: "get <address>",
17+
Short: "Get address summary including balances and transaction count",
18+
Args: cobra.ExactArgs(1),
19+
RunE: runGet,
20+
}
21+
}
22+
23+
func runGet(cmd *cobra.Command, args []string) error {
24+
client := cmdutil.ClientFromCmd(cmd)
25+
network := cmdutil.NetworkFromCmd(cmd)
26+
27+
params := ogen.GetV1AddressByAddressParams{Address: args[0]}
28+
if network != "" {
29+
params.Network.SetTo(ogen.GetV1AddressByAddressNetwork(network))
30+
}
31+
32+
resp, err := client.GetV1AddressByAddress(cmd.Context(), params)
33+
if err != nil {
34+
return err
35+
}
36+
37+
return output.PrintResult(cmd, resp, func(w io.Writer) error {
38+
fmt.Fprintf(w, "Spark Address: %s\n", resp.SparkAddress)
39+
fmt.Fprintf(w, "Public Key: %s\n", resp.PublicKey)
40+
fmt.Fprintf(w, "BTC Hard Balance: %d sats\n", resp.Balance.BtcHardBalanceSats)
41+
fmt.Fprintf(w, "BTC Soft Balance: %d sats\n", resp.Balance.BtcSoftBalanceSats)
42+
fmt.Fprintf(w, "BTC Value (Hard): $%.2f\n", resp.Balance.BtcValueUsdHard)
43+
fmt.Fprintf(w, "BTC Value (Soft): $%.2f\n", resp.Balance.BtcValueUsdSoft)
44+
fmt.Fprintf(w, "Token Value (USD): $%.2f\n", resp.Balance.TotalTokenValueUsd)
45+
fmt.Fprintf(w, "Total Value (USD): $%.2f\n", resp.TotalValueUsd)
46+
fmt.Fprintf(w, "Token Count: %d\n", resp.TokenCount)
47+
fmt.Fprintf(w, "Transaction Count: %d\n", resp.TransactionCount)
48+
return nil
49+
})
50+
}

0 commit comments

Comments
 (0)