diff --git a/rpc/wallet.go b/rpc/wallet.go new file mode 100644 index 0000000..4ae6c78 --- /dev/null +++ b/rpc/wallet.go @@ -0,0 +1,59 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package rpc + +import ( + "encoding/hex" + "fmt" +) + +// RandomOutputEntry is a decoy output returned by getrandom_outs. +type RandomOutputEntry struct { + GlobalIndex uint64 `json:"global_index"` + PublicKey string `json:"public_key"` +} + +// GetRandomOutputs fetches random decoy outputs for ring construction. +// Uses the legacy /getrandom_outs1 endpoint (not available via /json_rpc). +func (c *Client) GetRandomOutputs(amount uint64, count int) ([]RandomOutputEntry, error) { + params := struct { + Amount uint64 `json:"amount"` + Count int `json:"outs_count"` + }{Amount: amount, Count: count} + + var resp struct { + Outs []RandomOutputEntry `json:"outs"` + Status string `json:"status"` + } + + if err := c.legacyCall("/getrandom_outs1", params, &resp); err != nil { + return nil, err + } + if resp.Status != "OK" { + return nil, fmt.Errorf("getrandom_outs: status %q", resp.Status) + } + return resp.Outs, nil +} + +// SendRawTransaction submits a serialised transaction for relay. +// Uses the legacy /sendrawtransaction endpoint (not available via /json_rpc). +func (c *Client) SendRawTransaction(txBlob []byte) error { + params := struct { + TxAsHex string `json:"tx_as_hex"` + }{TxAsHex: hex.EncodeToString(txBlob)} + + var resp struct { + Status string `json:"status"` + } + + if err := c.legacyCall("/sendrawtransaction", params, &resp); err != nil { + return err + } + if resp.Status != "OK" { + return fmt.Errorf("sendrawtransaction: status %q", resp.Status) + } + return nil +} diff --git a/rpc/wallet_test.go b/rpc/wallet_test.go new file mode 100644 index 0000000..a689c94 --- /dev/null +++ b/rpc/wallet_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package rpc + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestGetRandomOutputs_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/getrandom_outs1" { + t.Errorf("path: got %s, want /getrandom_outs1", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "outs": [ + {"global_index": 10, "public_key": "aa00000000000000000000000000000000000000000000000000000000000000"}, + {"global_index": 20, "public_key": "bb00000000000000000000000000000000000000000000000000000000000000"} + ], + "status": "OK" + }`)) + })) + defer srv.Close() + + c := NewClient(srv.URL) + outs, err := c.GetRandomOutputs(1000, 2) + if err != nil { + t.Fatalf("GetRandomOutputs: %v", err) + } + if len(outs) != 2 { + t.Fatalf("outs: got %d, want 2", len(outs)) + } + if outs[0].GlobalIndex != 10 { + t.Errorf("outs[0].GlobalIndex: got %d, want 10", outs[0].GlobalIndex) + } + if outs[0].PublicKey != "aa00000000000000000000000000000000000000000000000000000000000000" { + t.Errorf("outs[0].PublicKey: got %q", outs[0].PublicKey) + } + if outs[1].GlobalIndex != 20 { + t.Errorf("outs[1].GlobalIndex: got %d, want 20", outs[1].GlobalIndex) + } +} + +func TestGetRandomOutputs_Bad_Status(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(struct{ Status string }{Status: "BUSY"}) + })) + defer srv.Close() + + c := NewClient(srv.URL) + _, err := c.GetRandomOutputs(1000, 2) + if err == nil { + t.Fatal("expected error for non-OK status") + } +} + +func TestSendRawTransaction_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/sendrawtransaction" { + t.Errorf("path: got %s, want /sendrawtransaction", r.URL.Path) + } + + var req struct { + TxAsHex string `json:"tx_as_hex"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("decode request: %v", err) + } + if req.TxAsHex != "0102" { + t.Errorf("tx_as_hex: got %q, want %q", req.TxAsHex, "0102") + } + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status":"OK"}`)) + })) + defer srv.Close() + + c := NewClient(srv.URL) + err := c.SendRawTransaction([]byte{0x01, 0x02}) + if err != nil { + t.Fatalf("SendRawTransaction: %v", err) + } +} + +func TestSendRawTransaction_Bad_Rejected(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(struct{ Status string }{Status: "Failed"}) + })) + defer srv.Close() + + c := NewClient(srv.URL) + err := c.SendRawTransaction([]byte{0x01}) + if err == nil { + t.Fatal("expected error for rejected transaction") + } +}