initial commit
This commit is contained in:
31
electrum/address.go
Normal file
31
electrum/address.go
Normal 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
29
electrum/address_test.go
Normal 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
91
electrum/block.go
Normal 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
64
electrum/misc.go
Normal 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
278
electrum/network.go
Normal 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
89
electrum/scripthash.go
Normal 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
109
electrum/server.go
Normal 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
250
electrum/subscribe.go
Normal 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
157
electrum/transaction.go
Normal 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
101
electrum/transport.go
Normal 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()
|
||||
}
|
Reference in New Issue
Block a user