Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ cmds = \
hemictl \
keygen \
popmd \
tbcd
tbcd \
twcd

.PHONY: all clean deps go-deps $(cmds) build install lint lint-deps tidy race test vulncheck \
vulncheck-deps
Expand Down
141 changes: 141 additions & 0 deletions api/twcapi/twcapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright (c) 2025 Hemi Labs, Inc.
// Use of this source code is governed by the MIT License,
// which can be found in the LICENSE file.

package twcapi

import (
"context"
"fmt"
"maps"
"reflect"

"github.com/hemilabs/heminetwork/api"
"github.com/hemilabs/heminetwork/api/protocol"
)

const (
APIVersion = 1

CmdPingRequest = "twcapi-ping-request"
CmdPingResponse = "twcapi-ping-response"

// CmdBTCNewBlockNotification = "twcapi-btc-new-block-notification"
CmdBitcoinBalanceRequest = "twcapi-bitcoin-balance-request"
CmdBitcoinBalanceResponse = "twcapi-bitcoin-balance-response"
CmdBitcoinBroadcastRequest = "twcapi-bitcoin-broadcast-request"
CmdBitcoinBroadcastResponse = "twcapi-bitcoin-broadcast-response"
CmdBitcoinInfoRequest = "twcapi-bitcoin-info-request"
CmdBitcoinInfoResponse = "twcapi-bitcoin-info-response"
CmdBitcoinUTXOsRequest = "twcapi-bitcoin-utxos-request"
CmdBitcoinUTXOsResponse = "twcapi-bitcoin-utxos-response"
)

var (
APIVersionRoute = fmt.Sprintf("v%d", APIVersion)
RouteWebsocket = fmt.Sprintf("/%s/ws", APIVersionRoute)

DefaultListen = "localhost:8083" // XXX confirm port is ok
DefaultURL = fmt.Sprintf("ws://%s/%s", DefaultListen, RouteWebsocket)
)

type (
PingRequest protocol.PingRequest
PingResponse protocol.PingResponse
)

// type BTCNewBlockNotification struct{}
type BitcoinBalanceRequest struct {
ScriptHash api.ByteSlice `json:"script_hash"`
}

type BitcoinBalanceResponse struct {
Confirmed uint64 `json:"confirmed"`
Unconfirmed int64 `json:"unconfirmed"`
Error *protocol.Error `json:"error,omitempty"`
}

type BitcoinBroadcastRequest struct {
Transaction api.ByteSlice `json:"transaction"` // XXX this needs to be plural
}

type BitcoinBroadcastResponse struct {
TXID api.ByteSlice `json:"txid"`
Error *protocol.Error `json:"error,omitempty"`
}

type BitcoinInfoRequest struct{}

type BitcoinInfoResponse struct {
Height uint64 `json:"height"`
Error *protocol.Error `json:"error,omitempty"`
}
type BitcoinUTXO struct {
Hash api.ByteSlice `json:"hash"`
Index uint32 `json:"index"`
Value int64 `json:"value"`
}

type BitcoinUTXOsRequest struct {
ScriptHash api.ByteSlice `json:"script_hash"`
}

type BitcoinUTXOsResponse struct {
UTXOs []*BitcoinUTXO `json:"utxos"`
Error *protocol.Error `json:"error,omitempty"`
}

var commands = map[protocol.Command]reflect.Type{
CmdPingRequest: reflect.TypeOf(PingRequest{}),
CmdPingResponse: reflect.TypeOf(PingResponse{}),

// CmdBTCNewBlockNotification: reflect.TypeOf(BTCNewBlockNotification{}),
CmdBitcoinBalanceRequest: reflect.TypeOf(BitcoinBalanceRequest{}),
CmdBitcoinBalanceResponse: reflect.TypeOf(BitcoinBalanceResponse{}),
CmdBitcoinBroadcastRequest: reflect.TypeOf(BitcoinBroadcastRequest{}),
CmdBitcoinBroadcastResponse: reflect.TypeOf(BitcoinBroadcastResponse{}),
CmdBitcoinInfoRequest: reflect.TypeOf(BitcoinInfoRequest{}),
CmdBitcoinInfoResponse: reflect.TypeOf(BitcoinInfoResponse{}),
CmdBitcoinUTXOsRequest: reflect.TypeOf(BitcoinUTXOsRequest{}),
CmdBitcoinUTXOsResponse: reflect.TypeOf(BitcoinUTXOsResponse{}),
}

type twcAPI struct{}

func (a *twcAPI) Commands() map[protocol.Command]reflect.Type {
return commands
}

func APICommands() map[protocol.Command]reflect.Type {
return maps.Clone(commands)
}

// Write is the low level primitive of a protocol Write. One should generally
// not use this function and use WriteConn and Call instead.
func Write(ctx context.Context, c protocol.APIConn, id string, payload any) error {
return protocol.Write(ctx, c, &twcAPI{}, id, payload)
}

// Read is the low level primitive of a protocol Read. One should generally
// not use this function and use ReadConn instead.
func Read(ctx context.Context, c protocol.APIConn) (protocol.Command, string, any, error) {
return protocol.Read(ctx, c, &twcAPI{})
}

// Call is a blocking call. One should use ReadConn when using Call or else the
// completion will end up in the Read instead of being completed as expected.
func Call(ctx context.Context, c *protocol.Conn, payload any) (protocol.Command, string, any, error) {
return c.Call(ctx, &twcAPI{}, payload)
}

// WriteConn writes to Conn. It is equivalent to Write but exists for symmetry
// reasons.
func WriteConn(ctx context.Context, c *protocol.Conn, id string, payload any) error {
return c.Write(ctx, &twcAPI{}, id, payload)
}

// ReadConn reads from Conn and performs callbacks. One should use ReadConn over
// Read when mixing Write, WriteConn and Call.
func ReadConn(ctx context.Context, c *protocol.Conn) (protocol.Command, string, any, error) {
return c.Read(ctx, &twcAPI{})
}
20 changes: 19 additions & 1 deletion cmd/hemictl/hemictl.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2024 Hemi Labs, Inc.
// Copyright (c) 2024-2025 Hemi Labs, Inc.
// Use of this source code is governed by the MIT License,
// which can be found in the LICENSE file.

Expand Down Expand Up @@ -38,6 +38,7 @@ import (
"github.com/hemilabs/heminetwork/api/bssapi"
"github.com/hemilabs/heminetwork/api/protocol"
"github.com/hemilabs/heminetwork/api/tbcapi"
"github.com/hemilabs/heminetwork/api/twcapi"
"github.com/hemilabs/heminetwork/config"
"github.com/hemilabs/heminetwork/database/bfgd/postgres"
ldb "github.com/hemilabs/heminetwork/database/level"
Expand Down Expand Up @@ -109,6 +110,16 @@ func handleTBCWebsocketRead(ctx context.Context, conn *protocol.Conn) {
}
}

// handleTBCWebsocketRead discards all reads but has to exist in order to
// be able to use twcapi.Call.
func handleTWCWebsocketRead(ctx context.Context, conn *protocol.Conn) {
for {
if _, _, _, err := twcapi.ReadConn(ctx, conn); err != nil {
return
}
}
}

func bfgdb() error {
ctx, cancel := context.WithTimeout(context.Background(), callTimeout)
defer cancel()
Expand Down Expand Up @@ -1035,6 +1046,9 @@ func init() {
for k, v := range tbcapi.APICommands() {
allCommands[string(k)] = v
}
for k, v := range twcapi.APICommands() {
allCommands[string(k)] = v
}

sortedCommands = make([]string, 0, len(allCommands))
for k := range allCommands {
Expand Down Expand Up @@ -1154,6 +1168,10 @@ func _main() error {
u = tbcapi.DefaultURL
callHandler = handleTBCWebsocketRead
call = tbcapi.Call // XXX yuck?
case strings.HasPrefix(cmd, "twcapi"):
u = twcapi.DefaultURL
callHandler = handleTWCWebsocketRead
call = twcapi.Call // XXX probably yuck
default:
return fmt.Errorf("can't derive URL from command: %v", cmd)
}
Expand Down
118 changes: 118 additions & 0 deletions cmd/twcd/twcd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) 2025 Hemi Labs, Inc.
// Use of this source code is governed by the MIT License,
// which can be found in the LICENSE file.

package main

import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/juju/loggo"

"github.com/hemilabs/heminetwork/api/twcapi"
"github.com/hemilabs/heminetwork/config"
"github.com/hemilabs/heminetwork/service/twc"
"github.com/hemilabs/heminetwork/version"
)

const (
daemonName = "twcd"
defaultLogLevel = daemonName + "=INFO;twc=INFO;level=INFO"
defaultNetwork = "testnet3" // XXX make this mainnet
defaultHome = "~/." + daemonName
bhsDefault = int(1e6) // enough for mainnet
)

var (
log = loggo.GetLogger(daemonName)
welcome string

cfg = twc.NewDefaultConfig()
cm = config.CfgMap{
"TWC_ADDRESS": config.Config{
Value: &cfg.ListenAddress,
DefaultValue: twcapi.DefaultListen,
Help: "address port to listen on",
Print: config.PrintAll,
},
}
)

func HandleSignals(ctx context.Context, cancel context.CancelFunc, callback func(os.Signal)) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
defer func() {
signal.Stop(signalChan)
cancel()
}()

select {
case <-ctx.Done():
case s := <-signalChan: // First signal, cancel context.
if callback != nil {
callback(s) // Do whatever caller wants first.
cancel()
}
}
<-signalChan // Second signal, hard exit.
os.Exit(2)
}

func _main() error {
// Parse configuration from environment
if err := config.Parse(cm); err != nil {
return err
}

if err := loggo.ConfigureLoggers(cfg.LogLevel); err != nil {
return err
}
log.Infof("%v", welcome)

pc := config.PrintableConfig(cm)
for k := range pc {
log.Infof("%v", pc[k])
}

ctx, cancel := context.WithCancel(context.Background())
go HandleSignals(ctx, cancel, func(s os.Signal) {
log.Infof("twc service received signal: %s", s)
})

server, err := twc.NewServer(cfg)
if err != nil {
return fmt.Errorf("create twc server: %w", err)
}

if err := server.Run(ctx); !errors.Is(err, context.Canceled) {
return fmt.Errorf("twc server terminated: %w", err)
}

return nil
}

func init() {
version.Component = "twcd"
welcome = "Hemi Tiny Wallet Daemon " + version.BuildInfo()
}

func main() {
if len(os.Args) != 1 {
fmt.Fprintf(os.Stderr, "%v\n", welcome)
fmt.Fprintf(os.Stderr, "Usage:\n")
fmt.Fprintf(os.Stderr, "\thelp (this help)\n")
fmt.Fprintf(os.Stderr, "Environment:\n")
config.Help(os.Stderr, cm)
os.Exit(1)
}

if err := _main(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
Loading