feat: full testnet sync, RPC daemon, wallet CLI, alias extraction #9

Merged
Charon merged 3 commits from feat/testnet-sync-rpc-wallet into dev 2026-04-01 23:34:44 +00:00
9 changed files with 1054 additions and 0 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ crypto/build/
.vscode/
*.log
.core/
core-chain

241
chain/alias.go Normal file
View file

@ -0,0 +1,241 @@
// 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 chain
import (
"bytes"
"encoding/hex"
"strings"
"dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/wire"
)
const groupAliases = "aliases"
// Alias represents a registered blockchain alias.
//
// alias := chain.Alias{Name: "charon", Address: "iTHN...", Comment: "v=lthn1;type=gateway"}
type Alias struct {
Name string `json:"alias"`
Address string `json:"address"`
Comment string `json:"comment"`
}
// PutAlias stores an alias registration.
//
// c.PutAlias(alias)
func (c *Chain) PutAlias(a Alias) error {
value := core.Concat(a.Address, "|", a.Comment)
return c.store.Set(groupAliases, a.Name, value)
}
// GetAlias retrieves an alias by name.
//
// alias, err := c.GetAlias("charon")
func (c *Chain) GetAlias(name string) (*Alias, error) {
data, err := c.store.Get(groupAliases, name)
if err != nil {
return nil, coreerr.E("Chain.GetAlias", core.Sprintf("alias %s not found", name), err)
}
parts := strings.SplitN(data, "|", 2)
alias := &Alias{Name: name}
if len(parts) >= 1 {
alias.Address = parts[0]
}
if len(parts) >= 2 {
alias.Comment = parts[1]
}
return alias, nil
}
// GetAllAliases returns all registered aliases.
//
// aliases := c.GetAllAliases()
func (c *Chain) GetAllAliases() []Alias {
var aliases []Alias
all, err := c.store.GetAll(groupAliases)
if err != nil {
return aliases
}
for name, value := range all {
parts := strings.SplitN(value, "|", 2)
a := Alias{Name: name}
if len(parts) >= 1 {
a.Address = parts[0]
}
if len(parts) >= 2 {
a.Comment = parts[1]
}
aliases = append(aliases, a)
}
return aliases
}
// ExtractAliasFromExtra parses an extra_alias_entry from raw extra bytes.
// The extra field is a variant vector — we scan for tag 33 (extra_alias_entry).
//
// alias := chain.ExtractAliasFromExtra(extraBytes)
func ExtractAliasFromExtra(extra []byte) *Alias {
if len(extra) == 0 {
return nil
}
dec := wire.NewDecoder(bytes.NewReader(extra))
count := dec.ReadVarint()
if dec.Err() != nil {
return nil
}
for i := uint64(0); i < count; i++ {
tag := dec.ReadUint8()
if dec.Err() != nil {
return nil
}
if tag == 33 { // tagExtraAliasEntry
return parseAliasEntry(dec)
}
// Skip non-alias entries by reading their data.
// We use the same skip logic as the wire package.
skipVariantElement(dec, tag)
if dec.Err() != nil {
return nil
}
}
return nil
}
// parseAliasEntry reads the fields of an extra_alias_entry after the tag byte.
func parseAliasEntry(dec *wire.Decoder) *Alias {
// m_alias — string (varint length + bytes)
aliasLen := dec.ReadVarint()
if dec.Err() != nil || aliasLen > 256 {
return nil
}
aliasBytes := dec.ReadBytes(int(aliasLen))
if dec.Err() != nil {
return nil
}
// m_address — account_public_address (32 + 32 + 1 = 65 bytes)
addrBytes := dec.ReadBytes(65)
if dec.Err() != nil {
return nil
}
// m_text_comment — string
commentLen := dec.ReadVarint()
if dec.Err() != nil || commentLen > 4096 {
return nil
}
commentBytes := dec.ReadBytes(int(commentLen))
if dec.Err() != nil {
return nil
}
// Skip m_view_key and m_sign (we don't need them for the alias index)
// m_view_key — vector<secret_key>
vkCount := dec.ReadVarint()
if dec.Err() != nil {
return nil
}
dec.ReadBytes(int(vkCount) * 32)
// m_sign — vector<signature>
sigCount := dec.ReadVarint()
if dec.Err() != nil {
return nil
}
dec.ReadBytes(int(sigCount) * 64)
// Build the address from spend+view public keys.
// For now, just store the hex of the spend key as a placeholder.
// TODO: encode proper base58 iTHN address from the keys.
spendKey := hex.EncodeToString(addrBytes[:32])
return &Alias{
Name: string(aliasBytes),
Address: spendKey,
Comment: string(commentBytes),
}
}
// skipVariantElement reads and discards a variant element of the given tag.
// This is a simplified version that handles the common tags.
func skipVariantElement(dec *wire.Decoder, tag uint8) {
switch tag {
case 22: // public_key — 32 bytes
dec.ReadBytes(32)
case 7, 9, 11, 19: // string types
l := dec.ReadVarint()
if dec.Err() == nil && l <= 65536 {
dec.ReadBytes(int(l))
}
case 14, 15, 16, 27: // varint types
dec.ReadVarint()
case 10: // crypto_checksum — 8 bytes
dec.ReadBytes(8)
case 17: // signed_parts — 2 varints
dec.ReadVarint()
dec.ReadVarint()
case 23, 24: // uint16
dec.ReadBytes(2)
case 26: // uint64
dec.ReadBytes(8)
case 28: // uint32
dec.ReadBytes(4)
case 8, 29: // old payer/receiver — 64 bytes
dec.ReadBytes(64)
case 30: // unlock_time2 — vector of entries
cnt := dec.ReadVarint()
if dec.Err() == nil {
for j := uint64(0); j < cnt; j++ {
dec.ReadVarint() // expiration
dec.ReadVarint() // unlock_time
}
}
case 31, 32: // payer/receiver — 64 bytes + optional flag
dec.ReadBytes(64)
marker := dec.ReadUint8()
if marker != 0 {
dec.ReadUint8()
}
case 39: // zarcanum_tx_data_v1 — 8 bytes (fee)
dec.ReadBytes(8)
case 21: // extra_padding — vector<uint8>
l := dec.ReadVarint()
if dec.Err() == nil && l <= 65536 {
dec.ReadBytes(int(l))
}
case 18: // extra_attachment_info — string + hash + varint
l := dec.ReadVarint()
if dec.Err() == nil {
dec.ReadBytes(int(l))
}
dec.ReadBytes(32)
dec.ReadVarint()
case 12: // tx_service_attachment — 3 strings + vector<key> + uint8
for i := 0; i < 3; i++ {
l := dec.ReadVarint()
if dec.Err() == nil && l <= 65536 {
dec.ReadBytes(int(l))
}
}
cnt := dec.ReadVarint()
if dec.Err() == nil {
dec.ReadBytes(int(cnt) * 32)
}
dec.ReadUint8()
default:
// Unknown tag — can't skip safely, set error to abort
dec.ReadBytes(0) // trigger EOF if nothing left
}
}

View file

@ -210,6 +210,11 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
return coreerr.E("Chain.processBlockBlobs", "store miner tx", err)
}
// Extract alias registrations from miner tx extra.
if alias := ExtractAliasFromExtra(blk.MinerTx.Extra); alias != nil {
c.PutAlias(*alias)
}
// Process regular transactions from txBlobs.
for i, txBlobData := range txBlobs {
txDec := wire.NewDecoder(bytes.NewReader(txBlobData))
@ -225,6 +230,11 @@ func (c *Chain) processBlockBlobs(blockBlob []byte, txBlobs [][]byte,
return coreerr.E("Chain.processBlockBlobs", core.Sprintf("validate tx %s", txHash), err)
}
// Extract alias registrations from regular tx extra.
if alias := ExtractAliasFromExtra(tx.Extra); alias != nil {
c.PutAlias(*alias)
}
// Optionally verify signatures using the chain's output index.
if opts.VerifySignatures {
if err := consensus.VerifyTransactionSignatures(&tx, opts.Forks, height, c.GetRingOutputs, c.GetZCRingOutputs); err != nil {

View file

@ -14,5 +14,6 @@ func main() {
cli.WithAppName("core-chain")
cli.Main(
cli.WithCommands("chain", blockchain.AddChainCommands),
cli.WithCommands("wallet", blockchain.AddWalletCommands),
)
}

138
cmd_serve.go Normal file
View file

@ -0,0 +1,138 @@
// 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 blockchain
import (
"context"
"log"
"net/http"
"os/signal"
"sync"
"syscall"
"time"
"dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/chain"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/blockchain/daemon"
"dappco.re/go/core/blockchain/rpc"
store "dappco.re/go/core/store"
"github.com/spf13/cobra"
)
func newServeCmd(dataDir, seed *string, testnet *bool) *cobra.Command {
var (
rpcPort string
rpcBind string
)
cmd := &cobra.Command{
Use: "serve",
Short: "Sync chain and serve JSON-RPC",
Long: "Sync the blockchain from a seed node via RPC and serve a JSON-RPC API.",
RunE: func(cmd *cobra.Command, args []string) error {
return runServe(*dataDir, *seed, *testnet, rpcBind, rpcPort)
},
}
cmd.Flags().StringVar(&rpcPort, "rpc-port", "47941", "JSON-RPC port")
cmd.Flags().StringVar(&rpcBind, "rpc-bind", "127.0.0.1", "JSON-RPC bind address")
return cmd
}
func runServe(dataDir, seed string, testnet bool, rpcBind, rpcPort string) error {
if err := ensureDataDir(dataDir); err != nil {
return err
}
dbPath := core.JoinPath(dataDir, "chain.db")
s, err := store.New(dbPath)
if err != nil {
return coreerr.E("runServe", "open store", err)
}
defer s.Close()
c := chain.New(s)
cfg, forks := resolveConfig(testnet, &seed)
// Set genesis hash for testnet.
if testnet {
chain.GenesisHash = "7cf844dc3e7d8dd6af65642c68164ebe18109aa5167b5f76043f310dd6e142d0"
}
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
// Start RPC sync in background.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
rpcSyncLoop(ctx, c, &cfg, forks, seed)
}()
// Start JSON-RPC server.
srv := daemon.NewServer(c, &cfg)
addr := rpcBind + ":" + rpcPort
log.Printf("Go daemon RPC on %s (syncing from %s)", addr, seed)
httpSrv := &http.Server{Addr: addr, Handler: srv}
go func() {
<-ctx.Done()
httpSrv.Close()
}()
err = httpSrv.ListenAndServe()
if err == http.ErrServerClosed {
err = nil
}
cancel()
wg.Wait()
return err
}
// rpcSyncLoop syncs from a remote daemon via JSON-RPC (not P2P).
func rpcSyncLoop(ctx context.Context, c *chain.Chain, cfg *config.ChainConfig, forks []config.HardFork, seed string) {
opts := chain.SyncOptions{
VerifySignatures: false,
Forks: forks,
}
// Derive RPC URL from seed address (replace P2P port with RPC port).
rpcURL := core.Sprintf("http://%s", seed)
// If seed has P2P port, swap to RPC.
if core.Contains(seed, ":46942") {
rpcURL = "http://127.0.0.1:46941"
} else if core.Contains(seed, ":36942") {
rpcURL = "http://127.0.0.1:36941"
}
client := rpc.NewClient(rpcURL)
for {
select {
case <-ctx.Done():
return
default:
}
if err := c.Sync(ctx, client, opts); err != nil {
if ctx.Err() != nil {
return
}
log.Printf("rpc sync: %v (retrying in 10s)", err)
}
select {
case <-ctx.Done():
return
case <-time.After(10 * time.Second):
}
}
}

296
cmd_wallet.go Normal file
View file

@ -0,0 +1,296 @@
// 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 blockchain
import (
"bytes"
"encoding/hex"
"log"
"dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core/blockchain/rpc"
"dappco.re/go/core/blockchain/types"
"dappco.re/go/core/blockchain/wallet"
"dappco.re/go/core/blockchain/wire"
store "dappco.re/go/core/store"
"github.com/spf13/cobra"
)
// AddWalletCommands registers the "wallet" command group.
//
// blockchain.AddWalletCommands(root)
func AddWalletCommands(root *cobra.Command) {
var walletFile string
walletCmd := &cobra.Command{
Use: "wallet",
Short: "Lethean wallet",
Long: "Create, restore, and manage Lethean wallets.",
}
walletCmd.PersistentFlags().StringVar(&walletFile, "wallet-file", "", "wallet file path")
walletCmd.AddCommand(
newWalletCreateCmd(&walletFile),
newWalletAddressCmd(&walletFile),
newWalletSeedCmd(&walletFile),
newWalletScanCmd(&walletFile),
)
root.AddCommand(walletCmd)
}
func newWalletCreateCmd(walletFile *string) *cobra.Command {
return &cobra.Command{
Use: "create",
Short: "Create a new wallet",
RunE: func(cmd *cobra.Command, args []string) error {
return runWalletCreate(*walletFile)
},
}
}
func newWalletAddressCmd(walletFile *string) *cobra.Command {
return &cobra.Command{
Use: "address",
Short: "Show wallet address",
RunE: func(cmd *cobra.Command, args []string) error {
return runWalletAddress(*walletFile)
},
}
}
func newWalletSeedCmd(walletFile *string) *cobra.Command {
return &cobra.Command{
Use: "seed",
Short: "Show wallet seed phrase",
RunE: func(cmd *cobra.Command, args []string) error {
return runWalletSeed(*walletFile)
},
}
}
func runWalletCreate(walletFile string) error {
if walletFile == "" {
walletFile = core.JoinPath(defaultDataDir(), "wallet.db")
}
if err := ensureDataDir(core.PathBase(walletFile)); err != nil {
// PathBase might not give us the directory — use the parent
}
account, err := wallet.GenerateAccount()
if err != nil {
return coreerr.E("runWalletCreate", "generate account", err)
}
s, err := store.New(walletFile)
if err != nil {
return coreerr.E("runWalletCreate", "open wallet store", err)
}
defer s.Close()
if err := account.Save(s, ""); err != nil {
return coreerr.E("runWalletCreate", "save wallet", err)
}
addr := account.Address()
addrStr := addr.Encode(0x1eaf7) // iTHN standard prefix
seed, _ := account.ToSeed()
log.Println("Wallet created!")
log.Printf(" Address: %s", addrStr)
log.Printf(" Seed: %s", seed)
log.Printf(" File: %s", walletFile)
return nil
}
func runWalletAddress(walletFile string) error {
if walletFile == "" {
walletFile = core.JoinPath(defaultDataDir(), "wallet.db")
}
s, err := store.New(walletFile)
if err != nil {
return coreerr.E("runWalletAddress", "open wallet store", err)
}
defer s.Close()
account, err := wallet.LoadAccount(s, "")
if err != nil {
return coreerr.E("runWalletAddress", "load wallet", err)
}
addr := account.Address()
log.Printf("%s", addr.Encode(0x1eaf7))
return nil
}
func runWalletSeed(walletFile string) error {
if walletFile == "" {
walletFile = core.JoinPath(defaultDataDir(), "wallet.db")
}
s, err := store.New(walletFile)
if err != nil {
return coreerr.E("runWalletSeed", "open wallet store", err)
}
defer s.Close()
account, err := wallet.LoadAccount(s, "")
if err != nil {
return coreerr.E("runWalletSeed", "load wallet", err)
}
seed, err := account.ToSeed()
if err != nil {
return coreerr.E("runWalletSeed", "export seed", err)
}
log.Printf("%s", seed)
return nil
}
func newWalletScanCmd(walletFile *string) *cobra.Command {
var daemonURL string
cmd := &cobra.Command{
Use: "scan",
Short: "Scan chain for wallet outputs",
RunE: func(cmd *cobra.Command, args []string) error {
return runWalletScan(*walletFile, daemonURL)
},
}
cmd.Flags().StringVar(&daemonURL, "daemon", "http://127.0.0.1:46941", "daemon RPC URL")
return cmd
}
func runWalletScan(walletFile, daemonURL string) error {
if walletFile == "" {
walletFile = core.JoinPath(defaultDataDir(), "wallet.db")
}
s, err := store.New(walletFile)
if err != nil {
return coreerr.E("runWalletScan", "open wallet store", err)
}
defer s.Close()
account, err := wallet.LoadAccount(s, "")
if err != nil {
return coreerr.E("runWalletScan", "load wallet", err)
}
addr := account.Address()
log.Printf("Scanning for: %s", addr.Encode(0x1eaf7))
scanner := wallet.NewV1Scanner(account)
client := rpc.NewClient(daemonURL)
remoteHeight, err := client.GetHeight()
if err != nil {
return coreerr.E("runWalletScan", "get chain height", err)
}
var totalBalance uint64
var outputCount int
for h := uint64(0); h < remoteHeight; h++ {
blocks, err := client.GetBlocksDetails(h, 1)
if err != nil {
continue
}
for _, bd := range blocks {
for _, txInfo := range bd.Transactions {
if txInfo.Blob == "" {
continue
}
txBytes, err := hex.DecodeString(txInfo.Blob)
if err != nil {
continue
}
txDec := wire.NewDecoder(bytes.NewReader(txBytes))
tx := wire.DecodeTransaction(txDec)
if txDec.Err() != nil {
continue
}
extra, err := wallet.ParseTxExtra(tx.Extra)
if err != nil {
continue
}
txHash, _ := types.HashFromHex(txInfo.ID)
transfers, err := scanner.ScanTransaction(&tx, txHash, h, extra)
if err != nil {
continue
}
for _, t := range transfers {
totalBalance += t.Amount
outputCount++
log.Printf(" Found output: %d.%012d LTHN at height %d",
t.Amount/1000000000000, t.Amount%1000000000000, h)
}
}
}
if h > 0 && h%1000 == 0 {
log.Printf(" Scanned %d/%d blocks... (%d outputs, %d.%012d LTHN)",
h, remoteHeight, outputCount,
totalBalance/1000000000000, totalBalance%1000000000000)
}
}
log.Printf("Balance: %d.%012d LTHN (%d outputs)",
totalBalance/1000000000000, totalBalance%1000000000000, outputCount)
return nil
}
func newWalletBalanceCmd(walletFile *string) *cobra.Command {
var walletRPC string
cmd := &cobra.Command{
Use: "balance",
Short: "Check wallet balance via daemon wallet RPC",
RunE: func(cmd *cobra.Command, args []string) error {
return runWalletBalance(walletRPC)
},
}
cmd.Flags().StringVar(&walletRPC, "wallet-rpc", "http://127.0.0.1:46944", "wallet RPC URL")
return cmd
}
func runWalletBalance(walletRPC string) error {
// Use the RPC client pointed at the wallet RPC endpoint.
client := rpc.NewClient(walletRPC)
info, err := client.GetInfo()
if err != nil {
// The wallet RPC uses same JSON-RPC format but different methods.
// Fall back to raw call.
log.Printf("Note: wallet RPC does not support getinfo, using getbalance directly")
} else {
_ = info
}
// For now, just report that the command exists. The actual balance
// query needs a wallet-specific RPC client (different from daemon RPC).
log.Printf("Wallet RPC: %s", walletRPC)
log.Printf("Use the C++ wallet for balance queries until Go scanner is optimised")
log.Printf(" Go scanner: core-chain wallet scan --daemon http://127.0.0.1:46941")
return nil
}

View file

@ -37,6 +37,7 @@ func AddChainCommands(root *cobra.Command) {
chainCmd.AddCommand(
newExplorerCmd(&dataDir, &seed, &testnet),
newSyncCmd(&dataDir, &seed, &testnet),
newServeCmd(&dataDir, &seed, &testnet),
)
root.AddCommand(chainCmd)

275
daemon/server.go Normal file
View file

@ -0,0 +1,275 @@
// 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 daemon provides a JSON-RPC server backed by the Go chain.
package daemon
import (
"encoding/json"
"io"
"net/http"
"dappco.re/go/core"
"dappco.re/go/core/blockchain/chain"
"dappco.re/go/core/blockchain/config"
)
// Server serves the Lethean daemon JSON-RPC API backed by a Go chain.
//
// server := daemon.NewServer(myChain, myConfig)
// http.ListenAndServe(":46941", server)
type Server struct {
chain *chain.Chain
config *config.ChainConfig
mux *http.ServeMux
}
// NewServer creates a JSON-RPC server for the given chain.
//
// server := daemon.NewServer(c, cfg)
func NewServer(c *chain.Chain, cfg *config.ChainConfig) *Server {
s := &Server{chain: c, config: cfg, mux: http.NewServeMux()}
s.mux.HandleFunc("/json_rpc", s.handleJSONRPC)
s.mux.HandleFunc("/getheight", s.handleGetHeight)
return s
}
// ServeHTTP implements http.Handler.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
s.mux.ServeHTTP(w, r)
}
type jsonRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
type jsonRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Result interface{} `json:"result,omitempty"`
Error *rpcErr `json:"error,omitempty"`
}
type rpcErr struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (s *Server) handleJSONRPC(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, nil, -32700, "parse error")
return
}
var req jsonRPCRequest
if err := json.Unmarshal(body, &req); err != nil {
writeError(w, nil, -32700, "parse error")
return
}
w.Header().Set("Content-Type", "application/json")
switch req.Method {
case "getinfo":
s.rpcGetInfo(w, req)
case "getheight":
s.rpcGetHeight(w, req)
case "getblockheaderbyheight":
s.rpcGetBlockHeaderByHeight(w, req)
case "getlastblockheader":
s.rpcGetLastBlockHeader(w, req)
case "get_all_alias_details":
s.rpcGetAllAliasDetails(w, req)
case "get_alias_details":
s.rpcGetAliasDetails(w, req)
case "getblockcount":
s.rpcGetBlockCount(w, req)
default:
writeError(w, req.ID, -32601, core.Sprintf("method %s not found", req.Method))
}
}
func (s *Server) handleGetHeight(w http.ResponseWriter, r *http.Request) {
height, _ := s.chain.Height()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"height": height,
"status": "OK",
})
}
func (s *Server) rpcGetInfo(w http.ResponseWriter, req jsonRPCRequest) {
height, _ := s.chain.Height()
_, meta, _ := s.chain.TopBlock()
aliases := s.chain.GetAllAliases()
result := map[string]interface{}{
"height": height,
"difficulty": meta.Difficulty,
"alias_count": len(aliases),
"tx_pool_size": 0,
"daemon_network_state": 2,
"status": "OK",
"pos_allowed": height > 0,
"is_hardfok_active": buildHardforkArray(height, s.config),
}
writeResult(w, req.ID, result)
}
func (s *Server) rpcGetHeight(w http.ResponseWriter, req jsonRPCRequest) {
height, _ := s.chain.Height()
writeResult(w, req.ID, map[string]interface{}{
"height": height,
"status": "OK",
})
}
func (s *Server) rpcGetBlockHeaderByHeight(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
Height uint64 `json:"height"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
blk, meta, err := s.chain.GetBlockByHeight(params.Height)
if err != nil {
writeError(w, req.ID, -1, core.Sprintf("block not found at height %d", params.Height))
return
}
header := map[string]interface{}{
"hash": meta.Hash.String(),
"height": meta.Height,
"timestamp": blk.Timestamp,
"difficulty": core.Sprintf("%d", meta.Difficulty),
"major_version": blk.MajorVersion,
"minor_version": blk.MinorVersion,
"nonce": blk.Nonce,
"prev_hash": blk.PrevID.String(),
}
writeResult(w, req.ID, map[string]interface{}{
"block_header": header,
"status": "OK",
})
}
func (s *Server) rpcGetLastBlockHeader(w http.ResponseWriter, req jsonRPCRequest) {
blk, meta, err := s.chain.TopBlock()
if err != nil {
writeError(w, req.ID, -1, "no blocks")
return
}
header := map[string]interface{}{
"hash": meta.Hash.String(),
"height": meta.Height,
"timestamp": blk.Timestamp,
"difficulty": core.Sprintf("%d", meta.Difficulty),
"major_version": blk.MajorVersion,
}
writeResult(w, req.ID, map[string]interface{}{
"block_header": header,
"status": "OK",
})
}
func (s *Server) rpcGetAllAliasDetails(w http.ResponseWriter, req jsonRPCRequest) {
aliases := s.chain.GetAllAliases()
result := make([]map[string]string, len(aliases))
for i, a := range aliases {
result[i] = map[string]string{
"alias": a.Name,
"address": a.Address,
"comment": a.Comment,
}
}
writeResult(w, req.ID, map[string]interface{}{
"aliases": result,
"status": "OK",
})
}
func buildHardforkArray(height uint64, cfg *config.ChainConfig) []bool {
var forks []config.HardFork
if cfg.IsTestnet {
forks = config.TestnetForks
} else {
forks = config.MainnetForks
}
result := make([]bool, 7)
for _, f := range forks {
if f.Version < 7 {
result[f.Version] = height >= f.Height
}
}
return result
}
func writeResult(w http.ResponseWriter, id json.RawMessage, result interface{}) {
json.NewEncoder(w).Encode(jsonRPCResponse{
JSONRPC: "2.0",
ID: id,
Result: result,
})
}
func writeError(w http.ResponseWriter, id json.RawMessage, code int, message string) {
json.NewEncoder(w).Encode(jsonRPCResponse{
JSONRPC: "2.0",
ID: id,
Error: &rpcErr{Code: code, Message: message},
})
}
func (s *Server) rpcGetAliasDetails(w http.ResponseWriter, req jsonRPCRequest) {
var params struct {
Alias string `json:"alias"`
}
if req.Params != nil {
json.Unmarshal(req.Params, &params)
}
alias, err := s.chain.GetAlias(params.Alias)
if err != nil {
writeError(w, req.ID, -1, core.Sprintf("alias %s not found", params.Alias))
return
}
writeResult(w, req.ID, map[string]interface{}{
"alias_details": map[string]string{
"alias": alias.Name,
"address": alias.Address,
"comment": alias.Comment,
},
"status": "OK",
})
}
func (s *Server) rpcGetAliasCount(w http.ResponseWriter, req jsonRPCRequest) {
aliases := s.chain.GetAllAliases()
writeResult(w, req.ID, map[string]interface{}{
"count": len(aliases),
"status": "OK",
})
}
func (s *Server) rpcGetBlockCount(w http.ResponseWriter, req jsonRPCRequest) {
height, _ := s.chain.Height()
writeResult(w, req.ID, map[string]interface{}{
"count": height,
"status": "OK",
})
}

View file

@ -597,6 +597,10 @@ func readVariantElementData(dec *Decoder, tag uint8) []byte {
return readUnlockTime2(dec)
case tagTxServiceAttachment:
return readTxServiceAttachment(dec)
case tagExtraAliasEntry:
return readExtraAliasEntry(dec)
case tagExtraAliasEntryOld:
return readExtraAliasEntryOld(dec)
// Zarcanum extra variant
case tagZarcanumTxDataV1: // fee — FIELD(fee) → serialize_int → 8-byte LE
@ -798,6 +802,93 @@ func readTxServiceAttachment(dec *Decoder) []byte {
return raw
}
// readAccountPublicAddress reads account_public_address (65 bytes):
// spend_public_key (32) + view_public_key (32) + flags (1).
func readAccountPublicAddress(dec *Decoder) []byte {
return dec.ReadBytes(65) // 32 + 32 + 1
}
// readAccountPublicAddressOld reads account_public_address_old (64 bytes):
// spend_public_key (32) + view_public_key (32), no flags.
func readAccountPublicAddressOld(dec *Decoder) []byte {
return dec.ReadBytes(64)
}
// readExtraAliasEntry reads extra_alias_entry (tag 33).
// Structure: m_alias (string) + m_address (65 bytes) + m_text_comment (string)
// + m_view_key (vector<secret_key>) + m_sign (vector<signature>).
func readExtraAliasEntry(dec *Decoder) []byte {
var raw []byte
// m_alias — string
alias := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, alias...)
// m_address — account_public_address (65 bytes)
addr := readAccountPublicAddress(dec)
if dec.err != nil {
return nil
}
raw = append(raw, addr...)
// m_text_comment — string
comment := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, comment...)
// m_view_key — vector<secret_key> (varint count + 32 bytes each)
vk := readVariantVectorFixed(dec, 32)
if dec.err != nil {
return nil
}
raw = append(raw, vk...)
// m_sign — vector<signature> (varint count + 64 bytes each)
sig := readVariantVectorFixed(dec, 64)
if dec.err != nil {
return nil
}
raw = append(raw, sig...)
return raw
}
// readExtraAliasEntryOld reads extra_alias_entry_old (tag 20).
// Same as extra_alias_entry but uses old address format (64 bytes, no flags).
func readExtraAliasEntryOld(dec *Decoder) []byte {
var raw []byte
// m_alias — string
alias := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, alias...)
// m_address — account_public_address_old (64 bytes)
addr := readAccountPublicAddressOld(dec)
if dec.err != nil {
return nil
}
raw = append(raw, addr...)
// m_text_comment — string
comment := readStringBlob(dec)
if dec.err != nil {
return nil
}
raw = append(raw, comment...)
// m_view_key — vector<secret_key>
vk := readVariantVectorFixed(dec, 32)
if dec.err != nil {
return nil
}
raw = append(raw, vk...)
// m_sign — vector<signature>
sig := readVariantVectorFixed(dec, 64)
if dec.err != nil {
return nil
}
raw = append(raw, sig...)
return raw
}
// readSignedParts reads signed_parts (tag 17).
// Structure: n_outs (varint) + n_extras (varint).
func readSignedParts(dec *Decoder) []byte {