From 88d1e0bdb39d13d79129038762beb0910cc378dd Mon Sep 17 00:00:00 2001 From: Steven Polley Date: Thu, 14 Aug 2025 19:49:29 -0600 Subject: [PATCH] initial commit --- .gitignore | 1 + LICENSE | 34 +++-- README.md | 71 +++++++++- electrum/address.go | 31 +++++ electrum/address_test.go | 29 ++++ electrum/block.go | 91 +++++++++++++ electrum/misc.go | 64 +++++++++ electrum/network.go | 278 +++++++++++++++++++++++++++++++++++++++ electrum/scripthash.go | 89 +++++++++++++ electrum/server.go | 109 +++++++++++++++ electrum/subscribe.go | 250 +++++++++++++++++++++++++++++++++++ electrum/transaction.go | 157 ++++++++++++++++++++++ electrum/transport.go | 101 ++++++++++++++ example/singleserver.go | 32 +++++ go.mod | 23 ++++ go.sum | 127 ++++++++++++++++++ media/logo.png | Bin 0 -> 42141 bytes 17 files changed, 1472 insertions(+), 15 deletions(-) create mode 100644 .gitignore create mode 100644 electrum/address.go create mode 100644 electrum/address_test.go create mode 100644 electrum/block.go create mode 100644 electrum/misc.go create mode 100644 electrum/network.go create mode 100644 electrum/scripthash.go create mode 100644 electrum/server.go create mode 100644 electrum/subscribe.go create mode 100644 electrum/transaction.go create mode 100644 electrum/transport.go create mode 100644 example/singleserver.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 media/logo.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE index c09285b..c1964d7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,18 +1,24 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2025 steven +Copyright (c) 2025 Steven Polley +Copyright (c) 2022 Roman Maklakov +Copyright (c) 2019 Ian DescĂ´teaux +Copyright (c) 2015 Tristan Rice -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 42c93ea..68e3447 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ -# go-electrum +# go-electrum +A pure Go [Electrum](https://electrum.org/) bitcoin library supporting the latest [ElectrumX](https://github.com/spesmilo/electrumx) protocol versions. +This makes it easy to write cryptocurrencies based services in a trustless fashion using Go without having to run a full node. + +![go-electrum](https://code.stevenpolley.net/steven/go-electrum/raw/branch/master/media/logo.png) + + + +## Usage +See [example/](https://code.stevenpolley.net/steven/go-electrum/src/branch/master/example) for more. + +### electrum +```bash +$ go get code.stevenpolley.net/steven/go-electrum +``` + +```go +package main + +import ( + "context" + "log" + "time" + + "code.stevenpolley.net/steven/go-electrum/electrum" +) + +func main() { + // Establishing a new SSL connection to an ElectrumX server + client := electrum.NewClient() + if err := client.ConnectTCP(context.TODO(), "satoshi.stevenpolley.net:50002"); err != nil { + log.Fatal(err) + } + ctx := context.TODO() + // Making sure connection is not closed with timed "client.ping" call + go func() { + for { + if err := client.Ping(ctx); err != nil { + log.Fatal(err) + } + time.Sleep(60 * time.Second) + } + }() + + // Making sure we declare to the server what protocol we want to use + if _, _, err := client.ServerVersion(ctx); err != nil { + log.Fatal(err) + } + + // Asking the server for the balance of address bc1qng5cyhc06q9pnnldv2jxw7ga7hz5q5g0c30gr4 + // We must use scripthash of the address now as explained in ElectrumX docs + scripthash, _ := electrum.AddressToElectrumScriptHash("bc1qng5cyhc06q9pnnldv2jxw7ga7hz5q5g0c30gr4") + balance, err := client.GetBalance(ctx, scripthash) + if err != nil { + log.Fatal(err) + } + log.Printf("Address confirmed balance: %+v", balance.Confirmed) + log.Printf("Address unconfirmed balance: %+v", balance.Unconfirmed) +} +``` + +# License +go-electrum is licensed under the MIT license. See LICENSE file for more details. + +Copyright (c) 2025 Steven Polley +Copyright (c) 2022 Roman Maklakov +Copyright (c) 2019 Ian DescĂ´teaux +Copyright (c) 2015 Tristan Rice + +Based on Tristan Rice [go-electrum](https://github.com/d4l3k/go-electrum) unmaintained library. diff --git a/electrum/address.go b/electrum/address.go new file mode 100644 index 0000000..a4253c9 --- /dev/null +++ b/electrum/address.go @@ -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 +} diff --git a/electrum/address_test.go b/electrum/address_test.go new file mode 100644 index 0000000..faba113 --- /dev/null +++ b/electrum/address_test.go @@ -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) + } +} diff --git a/electrum/block.go b/electrum/block.go new file mode 100644 index 0000000..5111d8b --- /dev/null +++ b/electrum/block.go @@ -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 +} diff --git a/electrum/misc.go b/electrum/misc.go new file mode 100644 index 0000000..cca0543 --- /dev/null +++ b/electrum/misc.go @@ -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 +} diff --git a/electrum/network.go b/electrum/network.go new file mode 100644 index 0000000..41825f9 --- /dev/null +++ b/electrum/network.go @@ -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 +} diff --git a/electrum/scripthash.go b/electrum/scripthash.go new file mode 100644 index 0000000..bfa810a --- /dev/null +++ b/electrum/scripthash.go @@ -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 +} diff --git a/electrum/server.go b/electrum/server.go new file mode 100644 index 0000000..ac30619 --- /dev/null +++ b/electrum/server.go @@ -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 +} diff --git a/electrum/subscribe.go b/electrum/subscribe.go new file mode 100644 index 0000000..5185d30 --- /dev/null +++ b/electrum/subscribe.go @@ -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 +} diff --git a/electrum/transaction.go b/electrum/transaction.go new file mode 100644 index 0000000..77cf184 --- /dev/null +++ b/electrum/transaction.go @@ -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 +} diff --git a/electrum/transport.go b/electrum/transport.go new file mode 100644 index 0000000..78df46b --- /dev/null +++ b/electrum/transport.go @@ -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() +} diff --git a/example/singleserver.go b/example/singleserver.go new file mode 100644 index 0000000..f028e8a --- /dev/null +++ b/example/singleserver.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "log" + "time" + + "code.stevenpolley.net/steven/go-electrum/electrum" +) + +func main() { + client, err := electrum.NewClientTCP(context.Background(), "bch.imaginary.cash:50001") + + if err != nil { + log.Fatal(err) + } + + serverVer, protocolVer, err := client.ServerVersion(context.Background()) + if err != nil { + log.Fatal(err) + } + log.Printf("Server version: %s [Protocol %s]", serverVer, protocolVer) + + go func() { + for { + if err := client.Ping(context.Background()); err != nil { + log.Fatal(err) + } + time.Sleep(60 * time.Second) + } + }() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4faaeb3 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module code.stevenpolley.net/steven/go-electrum + +go 1.24.6 + +require ( + github.com/btcsuite/btcd v0.24.2 + github.com/btcsuite/btcd/btcutil v1.1.6 + github.com/checksum0/go-electrum v0.0.0-20220912200153-b862ac442cf9 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/btcsuite/btclog v1.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/sys v0.35.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..82cd2e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,127 @@ +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= +github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= +github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= +github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btclog v1.0.0 h1:sEkpKJMmfGiyZjADwEIgB1NSwMyfdD1FB8v6+w1T0Ns= +github.com/btcsuite/btclog v1.0.0/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/checksum0/go-electrum v0.0.0-20220912200153-b862ac442cf9 h1:PEkrrCdN0F0wgeof+V8dwMabAYccVBgJfqysVdlT51U= +github.com/checksum0/go-electrum v0.0.0-20220912200153-b862ac442cf9/go.mod h1:EjLxYzaf/28gOdSRlifeLfjoOA6aUjtJZhwaZPnjL9c= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/media/logo.png b/media/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bbdd128da6fdf98897a57de396b79e27a20cb4b7 GIT binary patch literal 42141 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 literal 0 HcmV?d00001