Skip to content

Commit c6bea05

Browse files
committed
feat: on-chain ticket token holdings via ClawChain RPC
- market/client.go: GetChainAccount (claw_getAccount RPC) + GetChainTokenName (resolve token_id → ticker via DB) - tools/ticket.go: ticket_holdings tool for web console chat - cmd/main.go: clawwork status shows Token Holdings section - Keywords: holdings, token balance, 持仓, 代币余额
1 parent b45dded commit c6bea05

5 files changed

Lines changed: 173 additions & 3 deletions

File tree

cmd/clawwork/main.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/clawplaza/clawwork-cli/internal/i18n"
2828
"github.com/clawplaza/clawwork-cli/internal/knowledge"
2929
"github.com/clawplaza/clawwork-cli/internal/llm"
30+
"github.com/clawplaza/clawwork-cli/internal/market"
3031
"github.com/clawplaza/clawwork-cli/internal/miner"
3132
"github.com/clawplaza/clawwork-cli/internal/updater"
3233
"github.com/clawplaza/clawwork-cli/internal/web"
@@ -704,6 +705,36 @@ func runStatus(_ *cobra.Command, _ []string) error {
704705
fmt.Printf(tr("Session NFT hits: %d\n", "本次会话 NFT 命中: %d\n"), state.TotalHits)
705706
}
706707

708+
// Show on-chain token holdings (ClawChain)
709+
pubKey := cfg.Agent.PublicKey
710+
if pubKey == "" {
711+
if keys, _ := auth.LoadKeys(); keys != nil {
712+
pubKey = keys.PublicKey
713+
}
714+
}
715+
if pubKey != "" {
716+
mc := market.New("")
717+
chainAcc, err := mc.GetChainAccount(context.Background(), pubKey)
718+
if err == nil && chainAcc != nil && len(chainAcc.TokenBalances) > 0 {
719+
fmt.Print(tr("\n--- Token Holdings (ClawChain) ---\n", "\n--- 代币持仓 (ClawChain) ---\n"))
720+
for tokenID, balance := range chainAcc.TokenBalances {
721+
if balance == "0" {
722+
continue
723+
}
724+
ticker, _ := mc.GetChainTokenName(context.Background(), tokenID)
725+
if ticker != "" {
726+
fmt.Printf(" %s: %s\n", ticker, balance)
727+
} else {
728+
short := tokenID
729+
if len(short) > 12 {
730+
short = short[:8] + "..."
731+
}
732+
fmt.Printf(" %s: %s\n", short, balance)
733+
}
734+
}
735+
}
736+
}
737+
707738
return nil
708739
}
709740

internal/market/client.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,71 @@ func unwrap(body []byte, out any) error {
493493
return nil
494494
}
495495

496+
// ClawChain RPC endpoint
497+
const clawchainRPC = "https://aichain.clawplaza.ai"
498+
499+
// GetChainAccount fetches an account from ClawChain by public key (hex, no 0x prefix).
500+
func (c *Client) GetChainAccount(ctx context.Context, publicKeyHex string) (*ChainAccount, error) {
501+
addr := "0x" + publicKeyHex
502+
body, err := json.Marshal(map[string]any{
503+
"jsonrpc": "2.0",
504+
"method": "claw_getAccount",
505+
"params": []string{addr},
506+
"id": 1,
507+
})
508+
if err != nil {
509+
return nil, err
510+
}
511+
req, err := http.NewRequestWithContext(ctx, "POST", clawchainRPC, bytes.NewReader(body))
512+
if err != nil {
513+
return nil, err
514+
}
515+
req.Header.Set("Content-Type", "application/json")
516+
resp, err := c.httpClient.Do(req)
517+
if err != nil {
518+
return nil, fmt.Errorf("chain RPC failed: %w", err)
519+
}
520+
defer resp.Body.Close()
521+
var rpcResp struct {
522+
Result *ChainAccount `json:"result"`
523+
Error *struct {
524+
Message string `json:"message"`
525+
} `json:"error"`
526+
}
527+
if err := json.NewDecoder(resp.Body).Decode(&rpcResp); err != nil {
528+
return nil, fmt.Errorf("chain RPC parse error: %w", err)
529+
}
530+
if rpcResp.Error != nil {
531+
return nil, fmt.Errorf("chain: %s", rpcResp.Error.Message)
532+
}
533+
return rpcResp.Result, nil
534+
}
535+
536+
// GetChainTokenName resolves a chain token_id to its ticker by querying the tickets table.
537+
func (c *Client) GetChainTokenName(ctx context.Context, chainTokenID string) (string, error) {
538+
url := "https://ef.clawplaza.ai/rest/v1/tickets?chain_token_id=eq." + chainTokenID + "&select=ticker&limit=1"
539+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
540+
if err != nil {
541+
return "", err
542+
}
543+
req.Header.Set("apikey", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzczNTQyMjI0LCJleHAiOjE4OTM0NTYwMDB9.Nwgkgzq9bNJQT3TDZoRMsn2A8TGkmSpVbt_eqDIK41c")
544+
resp, err := c.httpClient.Do(req)
545+
if err != nil {
546+
return "", err
547+
}
548+
defer resp.Body.Close()
549+
var rows []struct {
550+
Ticker string `json:"ticker"`
551+
}
552+
if err := json.NewDecoder(resp.Body).Decode(&rows); err != nil {
553+
return "", err
554+
}
555+
if len(rows) == 0 {
556+
return "", nil
557+
}
558+
return rows[0].Ticker, nil
559+
}
560+
496561
// ListTickets fetches active tickets from the platform.
497562
func (c *Client) ListTickets(ctx context.Context, limit int) ([]TicketItem, error) {
498563
if limit <= 0 || limit > 20 {

internal/market/types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,16 @@ type SkillRequestsResponse struct {
390390
Limit int `json:"limit"`
391391
}
392392

393+
// ── ClawChain types ──
394+
395+
// ChainAccount is the response from claw_getAccount RPC.
396+
type ChainAccount struct {
397+
Address string `json:"address"`
398+
CWBalance string `json:"cw_balance_cw"`
399+
TokenBalances map[string]string `json:"token_balances"` // token_id (hex) → balance (string)
400+
Nonce int `json:"nonce"`
401+
}
402+
393403
// ── Ticket v3 types ──
394404

395405
// TicketItem represents an active ticket from the tickets table.

internal/tools/ticket.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"strings"
88

9+
"github.com/clawplaza/clawwork-cli/internal/config"
910
"github.com/clawplaza/clawwork-cli/internal/market"
1011
)
1112

@@ -83,6 +84,68 @@ func (t *ticketListTool) Call(ctx context.Context, argsJSON string) string {
8384
return sb.String()
8485
}
8586

87+
// ── ticket_holdings ──
88+
89+
type ticketHoldingsTool struct {
90+
client *market.Client
91+
}
92+
93+
func NewTicketHoldingsTool(mc *market.Client) Tool {
94+
return &ticketHoldingsTool{client: mc}
95+
}
96+
97+
func (t *ticketHoldingsTool) Def() ToolDef {
98+
return ToolDef{
99+
Name: "ticket_holdings",
100+
Description: "Check your on-chain Ticket token holdings on ClawChain. " +
101+
"Shows all Ticket tokens you've earned from contributions. " +
102+
"Use when the owner asks about token balances, holdings, or portfolio.",
103+
Parameters: ToolParameters{
104+
Type: "object",
105+
Properties: map[string]ToolProperty{},
106+
},
107+
}
108+
}
109+
110+
func (t *ticketHoldingsTool) Call(ctx context.Context, _ string) string {
111+
cfg, err := config.Load()
112+
if err != nil || cfg.Agent.PublicKey == "" {
113+
return "error: no ClawAuth public key configured"
114+
}
115+
acc, err := t.client.GetChainAccount(ctx, cfg.Agent.PublicKey)
116+
if err != nil {
117+
return fmt.Sprintf("error fetching chain account: %s", err)
118+
}
119+
if acc == nil || len(acc.TokenBalances) == 0 {
120+
return "No token holdings on ClawChain."
121+
}
122+
123+
var sb strings.Builder
124+
sb.WriteString("=== Token Holdings (ClawChain) ===\n\n")
125+
count := 0
126+
for tokenID, balance := range acc.TokenBalances {
127+
if balance == "0" {
128+
continue
129+
}
130+
count++
131+
ticker, _ := t.client.GetChainTokenName(ctx, tokenID)
132+
if ticker != "" {
133+
sb.WriteString(fmt.Sprintf("%s: %s tokens\n", ticker, balance))
134+
} else {
135+
short := tokenID
136+
if len(short) > 12 {
137+
short = short[:8] + "..."
138+
}
139+
sb.WriteString(fmt.Sprintf("%s: %s tokens\n", short, balance))
140+
}
141+
}
142+
if count == 0 {
143+
return "No token holdings on ClawChain."
144+
}
145+
sb.WriteString(fmt.Sprintf("\nCW Balance: %s CW\n", acc.CWBalance))
146+
return sb.String()
147+
}
148+
86149
// ── ticket_contribute ──
87150

88151
type ticketContributeTool struct {

internal/web/chat.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ var toolRoutes = []struct {
202202
"余额", "转账", "钱包"}, "cw"},
203203
{[]string{"market", "browse", "order", "swap", "listing",
204204
"市场", "挂单", "交易"}, "market"},
205-
{[]string{"ticket", "torch", "topic", "contribute", "prediction", "nous",
206-
"话题", "贡献", "预测", "参与"}, "ticket"},
205+
{[]string{"ticket", "torch", "topic", "contribute", "prediction", "nous", "holdings", "token balance",
206+
"话题", "贡献", "预测", "参与", "持仓", "代币余额"}, "ticket"},
207207
{[]string{"bug", "issue", "report", "feedback", "问题", "反馈", "报告"}, "platform"},
208208
}
209209

@@ -286,6 +286,7 @@ func (s *ChatSession) buildToolList(userMsg string) []tools.Tool {
286286
if cats["ticket"] {
287287
list = append(list, tools.NewTicketListTool(s.marketClient))
288288
list = append(list, tools.NewTicketContributeTool(s.marketClient))
289+
list = append(list, tools.NewTicketHoldingsTool(s.marketClient))
289290
}
290291
if cats["platform"] {
291292
list = append(list, tools.NewReportIssueTool(s.marketClient))
@@ -311,7 +312,7 @@ func (s *ChatSession) buildAllTools(list []tools.Tool) []tools.Tool {
311312
tools.NewSocialMessagesReadTool(mc), tools.NewSocialMomentsReadTool(mc), tools.NewSocialLikeMomentTool(mc),
312313
tools.NewSocialConnectionsTool(mc),
313314
tools.NewCWBalanceTool(mc), tools.NewCWTransferTool(mc), tools.NewCWHistoryTool(mc),
314-
tools.NewTicketListTool(mc), tools.NewTicketContributeTool(mc),
315+
tools.NewTicketListTool(mc), tools.NewTicketContributeTool(mc), tools.NewTicketHoldingsTool(mc),
315316
tools.NewReportIssueTool(mc),
316317
)
317318
return list

0 commit comments

Comments
 (0)