initial commit

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

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea

34
LICENSE
View File

@@ -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.

View File

@@ -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.

31
electrum/address.go Normal file
View File

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

29
electrum/address_test.go Normal file
View File

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

91
electrum/block.go Normal file
View File

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

64
electrum/misc.go Normal file
View File

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

278
electrum/network.go Normal file
View File

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

89
electrum/scripthash.go Normal file
View File

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

109
electrum/server.go Normal file
View File

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

250
electrum/subscribe.go Normal file
View File

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

157
electrum/transaction.go Normal file
View File

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

101
electrum/transport.go Normal file
View File

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

32
example/singleserver.go Normal file
View File

@@ -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)
}
}()
}

23
go.mod Normal file
View File

@@ -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
)

127
go.sum Normal file
View File

@@ -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=

BIN
media/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB