feat(blockchain): full testnet sync, RPC daemon, wallet CLI, alias extraction
- wire: add readExtraAliasEntry (tag 33) + readExtraAliasEntryOld (tag 20) Unblocks HF4+ Zarcanum transaction deserialization - chain: alias extraction from transaction extra fields (14/14 on testnet) - chain: alias storage and retrieval via go-store - daemon: JSON-RPC server (getinfo, getheight, getblockheaderbyheight, getlastblockheader, get_all_alias_details, get_alias_details) - cmd: `chain serve` — sync from seed + serve RPC simultaneously - cmd: `wallet create` — generate keys, iTHN address, 24-word seed - cmd: `wallet address` — show base58-encoded iTHN address - cmd: `wallet seed` — show mnemonic seed phrase - cmd: `wallet scan` — scan chain for owned outputs via ECDH derivation Tested: 11,263 blocks synced in 3m11s from live testnet. All HF0-HF4 validated. 14 aliases extracted. RPC serves correct heights, difficulties, block headers, and alias data. Wallet generates valid iTHN addresses. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
fb43a83167
commit
caf13a1937
9 changed files with 1007 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@ crypto/build/
|
|||
.vscode/
|
||||
*.log
|
||||
.core/
|
||||
core-chain
|
||||
|
|
|
|||
241
chain/alias.go
Normal file
241
chain/alias.go
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
138
cmd_serve.go
Normal 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):
|
||||
}
|
||||
}
|
||||
}
|
||||
259
cmd_wallet.go
Normal file
259
cmd_wallet.go
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
// 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
265
daemon/server.go
Normal file
265
daemon/server.go
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
// 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)
|
||||
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, ¶ms)
|
||||
}
|
||||
|
||||
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, ¶ms)
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue