initial commit

This commit is contained in:
2025-08-14 19:49:29 -06:00
parent 48a60f3f58
commit 88d1e0bdb3
17 changed files with 1472 additions and 15 deletions

31
electrum/address.go Normal file
View File

@@ -0,0 +1,31 @@
package electrum
import (
"crypto/sha256"
"encoding/hex"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
)
// AddressToElectrumScriptHash converts valid bitcoin address to electrum scriptHash sha256 encoded, reversed and encoded in hex
// https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes
func AddressToElectrumScriptHash(addressStr string) (string, error) {
address, err := btcutil.DecodeAddress(addressStr, &chaincfg.MainNetParams)
if err != nil {
return "", err
}
script, err := txscript.PayToAddrScript(address)
if err != nil {
return "", err
}
hashSum := sha256.Sum256(script)
for i, j := 0, len(hashSum)-1; i < j; i, j = i+1, j-1 {
hashSum[i], hashSum[j] = hashSum[j], hashSum[i]
}
return hex.EncodeToString(hashSum[:]), nil
}

29
electrum/address_test.go Normal file
View File

@@ -0,0 +1,29 @@
package electrum
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestAddressToElectrumScriptHash(t *testing.T) {
tests := []struct {
address string
wantScriptHash string
}{
{
address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
wantScriptHash: "8b01df4e368ea28f8dc0423bcf7a4923e3a12d307c875e47a0cfbf90b5c39161",
},
{
address: "34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo",
wantScriptHash: "2375f2bbf7815e3cdc835074b052d65c9b2f101bab28d37250cc96b2ed9a6809",
},
}
for _, tc := range tests {
scriptHash, err := AddressToElectrumScriptHash(tc.address)
require.NoError(t, err)
assert.Equal(t, scriptHash, tc.wantScriptHash)
}
}

91
electrum/block.go Normal file
View File

@@ -0,0 +1,91 @@
package electrum
import (
"context"
"errors"
)
var (
// ErrCheckpointHeight is thrown if the checkpoint height is smaller than the block height.
ErrCheckpointHeight = errors.New("checkpoint height must be greater than or equal to block height")
)
// GetBlockHeaderResp represents the response to GetBlockHeader().
type GetBlockHeaderResp struct {
Result *GetBlockHeaderResult `json:"result"`
}
// GetBlockHeaderResult represents the content of the result field in the response to GetBlockHeader().
type GetBlockHeaderResult struct {
Branch []string `json:"branch"`
Header string `json:"header"`
Root string `json:"root"`
}
// GetBlockHeader returns the block header at a specific height.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-block-header
func (s *Client) GetBlockHeader(ctx context.Context, height uint32, checkpointHeight ...uint32) (*GetBlockHeaderResult, error) {
if checkpointHeight != nil && checkpointHeight[0] != 0 {
if height > checkpointHeight[0] {
return nil, ErrCheckpointHeight
}
var resp GetBlockHeaderResp
err := s.request(ctx, "blockchain.block.header", []interface{}{height, checkpointHeight[0]}, &resp)
return resp.Result, err
}
var resp basicResp
err := s.request(ctx, "blockchain.block.header", []interface{}{height, 0}, &resp)
if err != nil {
return nil, err
}
result := &GetBlockHeaderResult{
Branch: nil,
Header: resp.Result,
Root: "",
}
return result, err
}
// GetBlockHeadersResp represents the response to GetBlockHeaders().
type GetBlockHeadersResp struct {
Result *GetBlockHeadersResult `json:"result"`
}
// GetBlockHeadersResult represents the content of the result field in the response to GetBlockHeaders().
type GetBlockHeadersResult struct {
Count uint32 `json:"count"`
Headers string `json:"hex"`
Max uint32 `json:"max"`
Branch []string `json:"branch,omitempty"`
Root string `json:"root,omitempty"`
}
// GetBlockHeaders return a concatenated chunk of block headers.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-block-headers
func (s *Client) GetBlockHeaders(ctx context.Context, startHeight, count uint32,
checkpointHeight ...uint32) (*GetBlockHeadersResult, error) {
var resp GetBlockHeadersResp
var err error
if checkpointHeight != nil && checkpointHeight[0] != 0 {
if (startHeight + (count - 1)) > checkpointHeight[0] {
return nil, ErrCheckpointHeight
}
err = s.request(ctx, "blockchain.block.headers", []interface{}{startHeight, count, checkpointHeight[0]}, &resp)
} else {
err = s.request(ctx, "blockchain.block.headers", []interface{}{startHeight, count, 0}, &resp)
}
if err != nil {
return nil, err
}
return resp.Result, err
}

64
electrum/misc.go Normal file
View File

@@ -0,0 +1,64 @@
package electrum
import "context"
type basicResp struct {
Result string `json:"result"`
}
// GetFeeResp represents the response to GetFee().
type GetFeeResp struct {
Result float32 `json:"result"`
}
// GetFee returns the estimated transaction fee per kilobytes for a transaction
// to be confirmed within a target number of blocks.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-estimatefee
func (s *Client) GetFee(ctx context.Context, target uint32) (float32, error) {
var resp GetFeeResp
err := s.request(ctx, "blockchain.estimatefee", []interface{}{target}, &resp)
if err != nil {
return -1, err
}
return resp.Result, err
}
// GetRelayFee returns the minimum fee a transaction must pay to be accepted into the
// remote server memory pool.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-relayfee
func (s *Client) GetRelayFee(ctx context.Context) (float32, error) {
var resp GetFeeResp
err := s.request(ctx, "blockchain.relayfee", []interface{}{}, &resp)
if err != nil {
return -1, err
}
return resp.Result, err
}
// GetFeeHistogramResp represents the response to GetFee().
type getFeeHistogramResp struct {
Result [][2]uint64 `json:"result"`
}
// GetFeeHistogram returns a histogram of the fee rates paid by transactions in the
// memory pool, weighted by transacation size.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#mempool-get-fee-histogram
func (s *Client) GetFeeHistogram(ctx context.Context) (map[uint32]uint64, error) {
var resp getFeeHistogramResp
err := s.request(ctx, "mempool.get_fee_histogram", []interface{}{}, &resp)
if err != nil {
return nil, err
}
feeMap := make(map[uint32]uint64)
for i := 0; i < len(resp.Result); i++ {
feeMap[uint32(resp.Result[i][0])] = resp.Result[i][1]
}
return feeMap, err
}

278
electrum/network.go Normal file
View File

@@ -0,0 +1,278 @@
package electrum
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"log"
"sync"
"sync/atomic"
)
const (
// ClientVersion identifies the client version/name to the remote server
ClientVersion = "go-electrum1.1"
// ProtocolVersion identifies the support protocol version to the remote server
ProtocolVersion = "1.4"
nl = byte('\n')
)
var (
// DebugMode provides debug output on communications with the remote server if enabled.
DebugMode bool
// ErrServerConnected throws an error if remote server is already connected.
ErrServerConnected = errors.New("server is already connected")
// ErrServerShutdown throws an error if remote server has shutdown.
ErrServerShutdown = errors.New("server has shutdown")
// ErrTimeout throws an error if request has timed out
ErrTimeout = errors.New("request timeout")
// ErrNotImplemented throws an error if this RPC call has not been implemented yet.
ErrNotImplemented = errors.New("RPC call is not implemented")
// ErrDeprecated throws an error if this RPC call is deprecated.
ErrDeprecated = errors.New("RPC call has been deprecated")
)
// Transport provides interface to server transport.
type Transport interface {
SendMessage([]byte) error
Responses() <-chan []byte
Errors() <-chan error
Close() error
}
type container struct {
content []byte
err error
}
// Client stores information about the remote server.
type Client struct {
transport Transport
handlers map[uint64]chan *container
handlersLock sync.RWMutex
pushHandlers map[string][]chan *container
pushHandlersLock sync.RWMutex
Error chan error
quit chan struct{}
nextID uint64
}
// NewClientTCP initialize a new client for remote server and connects to the remote server using TCP
func NewClientTCP(ctx context.Context, addr string) (*Client, error) {
transport, err := NewTCPTransport(ctx, addr)
if err != nil {
return nil, err
}
c := &Client{
handlers: make(map[uint64]chan *container),
pushHandlers: make(map[string][]chan *container),
Error: make(chan error),
quit: make(chan struct{}),
}
c.transport = transport
go c.listen()
return c, nil
}
// NewClientSSL initialize a new client for remote server and connects to the remote server using SSL
func NewClientSSL(ctx context.Context, addr string, config *tls.Config) (*Client, error) {
transport, err := NewSSLTransport(ctx, addr, config)
if err != nil {
return nil, err
}
c := &Client{
handlers: make(map[uint64]chan *container),
pushHandlers: make(map[string][]chan *container),
Error: make(chan error),
quit: make(chan struct{}),
}
c.transport = transport
go c.listen()
return c, nil
}
type apiErr struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *apiErr) Error() string {
return fmt.Sprintf("errNo: %d, errMsg: %s", e.Code, e.Message)
}
type response struct {
ID uint64 `json:"id"`
Method string `json:"method"`
Error string `json:"error"`
}
func (s *Client) listen() {
for {
if s.IsShutdown() {
break
}
if s.transport == nil {
break
}
select {
case <-s.quit:
return
case err := <-s.transport.Errors():
s.Error <- err
s.Shutdown()
case bytes := <-s.transport.Responses():
result := &container{
content: bytes,
}
msg := &response{}
err := json.Unmarshal(bytes, msg)
if err != nil {
if DebugMode {
log.Printf("Unmarshal received message failed: %v", err)
}
result.err = fmt.Errorf("Unmarshal received message failed: %v", err)
} else if msg.Error != "" {
result.err = errors.New(msg.Error)
}
if len(msg.Method) > 0 {
s.pushHandlersLock.RLock()
handlers := s.pushHandlers[msg.Method]
s.pushHandlersLock.RUnlock()
for _, handler := range handlers {
select {
case handler <- result:
default:
}
}
}
s.handlersLock.RLock()
c, ok := s.handlers[msg.ID]
s.handlersLock.RUnlock()
if ok {
// TODO: very rare case. fix this memory leak, when nobody will read channel (in case of error)
c <- result
}
}
}
}
func (s *Client) listenPush(method string) <-chan *container {
c := make(chan *container, 1)
s.pushHandlersLock.Lock()
s.pushHandlers[method] = append(s.pushHandlers[method], c)
s.pushHandlersLock.Unlock()
return c
}
type request struct {
ID uint64 `json:"id"`
Method string `json:"method"`
Params []interface{} `json:"params"`
}
func (s *Client) request(ctx context.Context, method string, params []interface{}, v interface{}) error {
select {
case <-s.quit:
return ErrServerShutdown
default:
}
msg := request{
ID: atomic.AddUint64(&s.nextID, 1),
Method: method,
Params: params,
}
bytes, err := json.Marshal(msg)
if err != nil {
return err
}
bytes = append(bytes, nl)
err = s.transport.SendMessage(bytes)
if err != nil {
s.Shutdown()
return err
}
c := make(chan *container, 1)
s.handlersLock.Lock()
s.handlers[msg.ID] = c
s.handlersLock.Unlock()
defer func() {
s.handlersLock.Lock()
delete(s.handlers, msg.ID)
s.handlersLock.Unlock()
}()
var resp *container
select {
case resp = <-c:
case <-ctx.Done():
return ErrTimeout
}
if resp.err != nil {
return resp.err
}
if v != nil {
err = json.Unmarshal(resp.content, v)
if err != nil {
return err
}
}
return nil
}
func (s *Client) Shutdown() {
if !s.IsShutdown() {
close(s.quit)
}
if s.transport != nil {
_ = s.transport.Close()
}
s.transport = nil
s.handlers = nil
s.pushHandlers = nil
}
func (s *Client) IsShutdown() bool {
select {
case <-s.quit:
return true
default:
}
return false
}

89
electrum/scripthash.go Normal file
View File

@@ -0,0 +1,89 @@
package electrum
import "context"
// GetBalanceResp represents the response to GetBalance().
type GetBalanceResp struct {
Result GetBalanceResult `json:"result"`
}
// GetBalanceResult represents the content of the result field in the response to GetBalance().
type GetBalanceResult struct {
Confirmed float64 `json:"confirmed"`
Unconfirmed float64 `json:"unconfirmed"`
}
// GetBalance returns the confirmed and unconfirmed balance for a scripthash.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-balance
func (s *Client) GetBalance(ctx context.Context, scripthash string) (GetBalanceResult, error) {
var resp GetBalanceResp
err := s.request(ctx, "blockchain.scripthash.get_balance", []interface{}{scripthash}, &resp)
if err != nil {
return GetBalanceResult{}, err
}
return resp.Result, err
}
// GetMempoolResp represents the response to GetHistory() and GetMempool().
type GetMempoolResp struct {
Result []*GetMempoolResult `json:"result"`
}
// GetMempoolResult represents the content of the result field in the response
// to GetHistory() and GetMempool().
type GetMempoolResult struct {
Hash string `json:"tx_hash"`
Height int32 `json:"height"`
Fee uint32 `json:"fee,omitempty"`
}
// GetHistory returns the confirmed and unconfirmed history for a scripthash.
func (s *Client) GetHistory(ctx context.Context, scripthash string) ([]*GetMempoolResult, error) {
var resp GetMempoolResp
err := s.request(ctx, "blockchain.scripthash.get_history", []interface{}{scripthash}, &resp)
if err != nil {
return nil, err
}
return resp.Result, err
}
// GetMempool returns the unconfirmed transacations of a scripthash.
func (s *Client) GetMempool(ctx context.Context, scripthash string) ([]*GetMempoolResult, error) {
var resp GetMempoolResp
err := s.request(ctx, "blockchain.scripthash.get_mempool", []interface{}{scripthash}, &resp)
if err != nil {
return nil, err
}
return resp.Result, err
}
// ListUnspentResp represents the response to ListUnspent()
type ListUnspentResp struct {
Result []*ListUnspentResult `json:"result"`
}
// ListUnspentResult represents the content of the result field in the response to ListUnspent()
type ListUnspentResult struct {
Height uint32 `json:"height"`
Position uint32 `json:"tx_pos"`
Hash string `json:"tx_hash"`
Value uint64 `json:"value"`
}
// ListUnspent returns an ordered list of UTXOs for a scripthash.
func (s *Client) ListUnspent(ctx context.Context, scripthash string) ([]*ListUnspentResult, error) {
var resp ListUnspentResp
err := s.request(ctx, "blockchain.scripthash.listunspent", []interface{}{scripthash}, &resp)
if err != nil {
return nil, err
}
return resp.Result, err
}

109
electrum/server.go Normal file
View File

@@ -0,0 +1,109 @@
package electrum
import "context"
// Ping send a ping to the target server to ensure it is responding and
// keeping the session alive.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-ping
func (s *Client) Ping(ctx context.Context) error {
err := s.request(ctx, "server.ping", []interface{}{}, nil)
return err
}
// ServerAddPeer adds your new server into the remote server own peers list.
// This should not be used if you are a client.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-add-peer
func (s *Client) ServerAddPeer(ctx context.Context, features *ServerFeaturesResult) error {
var resp basicResp
err := s.request(ctx, "server.add_peer", []interface{}{features}, &resp)
return err
}
// ServerBanner returns the banner for this remote server.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-banner
func (s *Client) ServerBanner(ctx context.Context) (string, error) {
var resp basicResp
err := s.request(ctx, "server.banner", []interface{}{}, &resp)
return resp.Result, err
}
// ServerDonation returns the donation address for this remote server
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-donation-address
func (s *Client) ServerDonation(ctx context.Context) (string, error) {
var resp basicResp
err := s.request(ctx, "server.donation_address", []interface{}{}, &resp)
return resp.Result, err
}
type host struct {
TCPPort uint16 `json:"tcp_port,omitempty"`
SSLPort uint16 `json:"ssl_port,omitempty"`
}
// ServerFeaturesResp represent the response to GetFeatures().
type ServerFeaturesResp struct {
Result *ServerFeaturesResult `json:"result"`
}
// ServerFeaturesResult represent the data sent or receive in RPC call "server.features" and
// "server.add_peer".
type ServerFeaturesResult struct {
GenesisHash string `json:"genesis_hash"`
Hosts map[string]host `json:"hosts"`
ProtocolMax string `json:"protocol_max"`
ProtocolMin string `json:"protocol_min"`
Pruning bool `json:"pruning,omitempty"`
ServerVersion string `json:"server_version"`
HashFunction string `json:"hash_function"`
}
// ServerFeatures returns a list of features and services supported by the remote server.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-features
func (s *Client) ServerFeatures(ctx context.Context) (*ServerFeaturesResult, error) {
var resp ServerFeaturesResp
err := s.request(ctx, "server.features", []interface{}{}, &resp)
return resp.Result, err
}
// ServerPeers returns a list of peers this remote server is aware of.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-peers-subscribe
func (s *Client) ServerPeers(ctx context.Context) (interface{}, error) {
resp := &struct {
Result [][]interface{} `json:"result"`
}{}
err := s.request(ctx, "server.peers.subscribe", []interface{}{}, &resp)
return resp.Result, err
}
// ServerVersionResp represent the response to ServerVersion().
type ServerVersionResp struct {
Result [2]string `json:"result"`
}
// ServerVersion identify the client to the server, and negotiate the protocol version.
// This call must be sent first, or the server will default to an older protocol version.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-version
func (s *Client) ServerVersion(ctx context.Context) (serverVer, protocolVer string, err error) {
var resp ServerVersionResp
err = s.request(ctx, "server.version", []interface{}{ClientVersion, ProtocolVersion}, &resp)
if err != nil {
serverVer = ""
protocolVer = ""
} else {
serverVer = resp.Result[0]
protocolVer = resp.Result[1]
}
return
}

250
electrum/subscribe.go Normal file
View File

@@ -0,0 +1,250 @@
package electrum
import (
"context"
"encoding/json"
"errors"
"sync"
)
// SubscribeHeadersResp represent the response to SubscribeHeaders().
type SubscribeHeadersResp struct {
Result *SubscribeHeadersResult `json:"result"`
}
// SubscribeHeadersNotif represent the notification to SubscribeHeaders().
type SubscribeHeadersNotif struct {
Params []*SubscribeHeadersResult `json:"params"`
}
// SubscribeHeadersResult represents the content of the result field in the response to SubscribeHeaders().
type SubscribeHeadersResult struct {
Height int32 `json:"height,omitempty"`
Hex string `json:"hex"`
}
// SubscribeHeaders subscribes to receive block headers notifications when new blocks are found.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
func (s *Client) SubscribeHeaders(ctx context.Context) (<-chan *SubscribeHeadersResult, error) {
var resp SubscribeHeadersResp
err := s.request(ctx, "blockchain.headers.subscribe", []interface{}{}, &resp)
if err != nil {
return nil, err
}
respChan := make(chan *SubscribeHeadersResult, 1)
respChan <- resp.Result
go func() {
for msg := range s.listenPush("blockchain.headers.subscribe") {
if msg.err != nil {
return
}
var resp SubscribeHeadersNotif
err := json.Unmarshal(msg.content, &resp)
if err != nil {
return
}
for _, param := range resp.Params {
respChan <- param
}
}
}()
return respChan, nil
}
// ScripthashSubscription ...
type ScripthashSubscription struct {
server *Client
notifChan chan *SubscribeNotif
subscribedSH []string
scripthashMap map[string]string
lock sync.RWMutex
}
// SubscribeNotif represent the notification to SubscribeScripthash() and SubscribeMasternode().
type SubscribeNotif struct {
Params [2]string `json:"params"`
}
// SubscribeScripthash ...
func (s *Client) SubscribeScripthash() (*ScripthashSubscription, <-chan *SubscribeNotif) {
sub := &ScripthashSubscription{
server: s,
notifChan: make(chan *SubscribeNotif, 1),
scripthashMap: make(map[string]string),
}
go func() {
for msg := range s.listenPush("blockchain.scripthash.subscribe") {
if msg.err != nil {
return
}
var resp SubscribeNotif
err := json.Unmarshal(msg.content, &resp)
if err != nil {
return
}
sub.lock.Lock()
for _, a := range sub.subscribedSH {
if a == resp.Params[0] {
sub.notifChan <- &resp
break
}
}
sub.lock.Unlock()
}
}()
return sub, sub.notifChan
}
// Add ...
func (sub *ScripthashSubscription) Add(ctx context.Context, scripthash string, address ...string) error {
var resp basicResp
err := sub.server.request(ctx, "blockchain.scripthash.subscribe", []interface{}{scripthash}, &resp)
if err != nil {
return err
}
if len(resp.Result) > 0 {
sub.notifChan <- &SubscribeNotif{[2]string{scripthash, resp.Result}}
}
sub.lock.Lock()
sub.subscribedSH = append(sub.subscribedSH[:], scripthash)
if len(address) > 0 {
sub.scripthashMap[scripthash] = address[0]
}
sub.lock.Unlock()
return nil
}
// GetAddress ...
func (sub *ScripthashSubscription) GetAddress(scripthash string) (string, error) {
address, ok := sub.scripthashMap[scripthash]
if ok {
return address, nil
}
return "", errors.New("scripthash not found in map")
}
// GetScripthash ...
func (sub *ScripthashSubscription) GetScripthash(address string) (string, error) {
var found bool
var scripthash string
for k, v := range sub.scripthashMap {
if v == address {
scripthash = k
found = true
}
}
if found {
return scripthash, nil
}
return "", errors.New("address not found in map")
}
// GetChannel ...
func (sub *ScripthashSubscription) GetChannel() <-chan *SubscribeNotif {
return sub.notifChan
}
// Remove ...
func (sub *ScripthashSubscription) Remove(scripthash string) error {
for i, v := range sub.subscribedSH {
if v == scripthash {
sub.lock.Lock()
sub.subscribedSH = append(sub.subscribedSH[:i], sub.subscribedSH[i+1:]...)
sub.lock.Unlock()
return nil
}
}
return errors.New("scripthash not found")
}
// RemoveAddress ...
func (sub *ScripthashSubscription) RemoveAddress(address string) error {
scripthash, err := sub.GetScripthash(address)
if err != nil {
return err
}
for i, v := range sub.subscribedSH {
if v == scripthash {
sub.lock.Lock()
sub.subscribedSH = append(sub.subscribedSH[:i], sub.subscribedSH[i+1:]...)
delete(sub.scripthashMap, scripthash)
sub.lock.Unlock()
return nil
}
}
return errors.New("scripthash not found")
}
// Resubscribe ...
func (sub *ScripthashSubscription) Resubscribe(ctx context.Context) error {
for _, v := range sub.subscribedSH {
err := sub.Add(ctx, v)
if err != nil {
return err
}
}
return nil
}
// SubscribeMasternode subscribes to receive notifications when a masternode status changes.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
func (s *Client) SubscribeMasternode(ctx context.Context, collateral string) (<-chan string, error) {
var resp basicResp
err := s.request(ctx, "blockchain.masternode.subscribe", []interface{}{collateral}, &resp)
if err != nil {
return nil, err
}
respChan := make(chan string, 1)
if len(resp.Result) > 0 {
respChan <- resp.Result
}
go func() {
for msg := range s.listenPush("blockchain.masternode.subscribe") {
if msg.err != nil {
return
}
var resp SubscribeNotif
err := json.Unmarshal(msg.content, &resp)
if err != nil {
return
}
for _, param := range resp.Params {
respChan <- param
}
}
}()
return respChan, nil
}

157
electrum/transaction.go Normal file
View File

@@ -0,0 +1,157 @@
package electrum
import "context"
// BroadcastTransaction sends a raw transaction to the remote server to
// be broadcasted on the server network.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-broadcast
func (s *Client) BroadcastTransaction(ctx context.Context, rawTx string) (string, error) {
resp := &basicResp{}
err := s.request(ctx, "blockchain.transaction.broadcast", []interface{}{rawTx}, &resp)
if err != nil {
return "", err
}
return resp.Result, nil
}
// GetTransactionResp represents the response to GetTransaction().
type GetTransactionResp struct {
Result *GetTransactionResult `json:"result"`
}
// GetTransactionResult represents the content of the result field in the response to GetTransaction().
type GetTransactionResult struct {
Blockhash string `json:"blockhash"`
Blocktime uint64 `json:"blocktime"`
Confirmations int32 `json:"confirmations"`
Hash string `json:"hash"`
Hex string `json:"hex"`
Locktime uint32 `json:"locktime"`
Size uint32 `json:"size"`
Time uint64 `json:"time"`
Version uint32 `json:"version"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
Merkle GetMerkleProofResult `json:"merkle,omitempty"` // For protocol v1.5 and up.
}
// Vin represents the input side of a transaction.
type Vin struct {
Coinbase string `json:"coinbase"`
ScriptSig *ScriptSig `json:"scriptsig"`
Sequence uint32 `json:"sequence"`
TxID string `json:"txid"`
Vout uint32 `json:"vout"`
}
// ScriptSig represents the signature script for that transaction input.
type ScriptSig struct {
Asm string `json:"asm"`
Hex string `json:"hex"`
}
// Vout represents the output side of a transaction.
type Vout struct {
N uint32 `json:"n"`
ScriptPubkey ScriptPubkey `json:"scriptpubkey"`
Value float64 `json:"value"`
}
// ScriptPubkey represents the script of that transaction output.
type ScriptPubkey struct {
Addresses []string `json:"addresses,omitempty"`
Asm string `json:"asm"`
Hex string `json:"hex,omitempty"`
ReqSigs uint32 `json:"reqsigs,omitempty"`
Type string `json:"type"`
}
// GetTransaction gets the detailed information for a transaction.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get
func (s *Client) GetTransaction(ctx context.Context, txHash string) (*GetTransactionResult, error) {
var resp GetTransactionResp
err := s.request(ctx, "blockchain.transaction.get", []interface{}{txHash, true}, &resp)
if err != nil {
return nil, err
}
return resp.Result, nil
}
// GetRawTransaction gets a raw encoded transaction.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get
func (s *Client) GetRawTransaction(ctx context.Context, txHash string) (string, error) {
var resp basicResp
err := s.request(ctx, "blockchain.transaction.get", []interface{}{txHash, false}, &resp)
if err != nil {
return "", err
}
return resp.Result, nil
}
// GetMerkleProofResp represents the response to GetMerkleProof().
type GetMerkleProofResp struct {
Result *GetMerkleProofResult `json:"result"`
}
// GetMerkleProofResult represents the content of the result field in the response to GetMerkleProof().
type GetMerkleProofResult struct {
Merkle []string `json:"merkle"`
Height uint32 `json:"block_height"`
Position uint32 `json:"pos"`
}
// GetMerkleProof returns the merkle proof for a confirmed transaction.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get-merkle
func (s *Client) GetMerkleProof(ctx context.Context, txHash string, height uint32) (*GetMerkleProofResult, error) {
var resp GetMerkleProofResp
err := s.request(ctx, "blockchain.transaction.get_merkle", []interface{}{txHash, height}, &resp)
if err != nil {
return nil, err
}
return resp.Result, err
}
// GetHashFromPosition returns the transaction hash for a specific position in a block.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-id-from-pos
func (s *Client) GetHashFromPosition(ctx context.Context, height, position uint32) (string, error) {
var resp basicResp
err := s.request(ctx, "blockchain.transaction.id_from_pos", []interface{}{height, position, false}, &resp)
if err != nil {
return "", err
}
return resp.Result, err
}
// GetMerkleProofFromPosResp represents the response to GetMerkleProofFromPosition().
type GetMerkleProofFromPosResp struct {
Result *GetMerkleProofFromPosResult `json:"result"`
}
// GetMerkleProofFromPosResult represents the content of the result field in the response
// to GetMerkleProofFromPosition().
type GetMerkleProofFromPosResult struct {
Hash string `json:"tx_hash"`
Merkle []string `json:"merkle"`
}
// GetMerkleProofFromPosition returns the merkle proof for a specific position in a block.
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-id-from-pos
func (s *Client) GetMerkleProofFromPosition(ctx context.Context, height, position uint32) (*GetMerkleProofFromPosResult, error) {
var resp GetMerkleProofFromPosResp
err := s.request(ctx, "blockchain.transaction.id_from_pos", []interface{}{height, position, true}, &resp)
if err != nil {
return nil, err
}
return resp.Result, err
}

101
electrum/transport.go Normal file
View File

@@ -0,0 +1,101 @@
package electrum
import (
"bufio"
"context"
"crypto/tls"
"log"
"net"
"time"
)
// TCPTransport store information about the TCP transport.
type TCPTransport struct {
conn net.Conn
responses chan []byte
errors chan error
}
// NewTCPTransport opens a new TCP connection to the remote server.
func NewTCPTransport(ctx context.Context, addr string) (*TCPTransport, error) {
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}
tcp := &TCPTransport{
conn: conn,
responses: make(chan []byte),
errors: make(chan error),
}
go tcp.listen()
return tcp, nil
}
// NewSSLTransport opens a new SSL connection to the remote server.
func NewSSLTransport(ctx context.Context, addr string, config *tls.Config) (*TCPTransport, error) {
dialer := tls.Dialer{
NetDialer: &net.Dialer{},
Config: config,
}
conn, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}
tcp := &TCPTransport{
conn: conn,
responses: make(chan []byte),
errors: make(chan error),
}
go tcp.listen()
return tcp, nil
}
func (t *TCPTransport) listen() {
defer t.conn.Close()
reader := bufio.NewReader(t.conn)
for {
line, err := reader.ReadBytes(nl)
if err != nil {
t.errors <- err
break
}
if DebugMode {
log.Printf("%s [debug] %s -> %s", time.Now().Format("2006-01-02 15:04:05"), t.conn.RemoteAddr(), line)
}
t.responses <- line
}
}
// SendMessage sends a message to the remote server through the TCP transport.
func (t *TCPTransport) SendMessage(body []byte) error {
if DebugMode {
log.Printf("%s [debug] %s <- %s", time.Now().Format("2006-01-02 15:04:05"), t.conn.RemoteAddr(), body)
}
_, err := t.conn.Write(body)
return err
}
// Responses returns chan to TCP transport responses.
func (t *TCPTransport) Responses() <-chan []byte {
return t.responses
}
// Errors returns chan to TCP transport errors.
func (t *TCPTransport) Errors() <-chan error {
return t.errors
}
func (t *TCPTransport) Close() error {
return t.conn.Close()
}