initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.idea
|
34
LICENSE
34
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.
|
||||
|
69
README.md
69
README.md
@@ -1,2 +1,71 @@
|
||||
# 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.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 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
31
electrum/address.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package electrum
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
)
|
||||
|
||||
// AddressToElectrumScriptHash converts valid bitcoin address to electrum scriptHash sha256 encoded, reversed and encoded in hex
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes
|
||||
func AddressToElectrumScriptHash(addressStr string) (string, error) {
|
||||
address, err := btcutil.DecodeAddress(addressStr, &chaincfg.MainNetParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
script, err := txscript.PayToAddrScript(address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hashSum := sha256.Sum256(script)
|
||||
|
||||
for i, j := 0, len(hashSum)-1; i < j; i, j = i+1, j-1 {
|
||||
hashSum[i], hashSum[j] = hashSum[j], hashSum[i]
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hashSum[:]), nil
|
||||
}
|
29
electrum/address_test.go
Normal file
29
electrum/address_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package electrum
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddressToElectrumScriptHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
address string
|
||||
wantScriptHash string
|
||||
}{
|
||||
{
|
||||
address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
|
||||
wantScriptHash: "8b01df4e368ea28f8dc0423bcf7a4923e3a12d307c875e47a0cfbf90b5c39161",
|
||||
},
|
||||
{
|
||||
address: "34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo",
|
||||
wantScriptHash: "2375f2bbf7815e3cdc835074b052d65c9b2f101bab28d37250cc96b2ed9a6809",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
scriptHash, err := AddressToElectrumScriptHash(tc.address)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, scriptHash, tc.wantScriptHash)
|
||||
}
|
||||
}
|
91
electrum/block.go
Normal file
91
electrum/block.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package electrum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrCheckpointHeight is thrown if the checkpoint height is smaller than the block height.
|
||||
ErrCheckpointHeight = errors.New("checkpoint height must be greater than or equal to block height")
|
||||
)
|
||||
|
||||
// GetBlockHeaderResp represents the response to GetBlockHeader().
|
||||
type GetBlockHeaderResp struct {
|
||||
Result *GetBlockHeaderResult `json:"result"`
|
||||
}
|
||||
|
||||
// GetBlockHeaderResult represents the content of the result field in the response to GetBlockHeader().
|
||||
type GetBlockHeaderResult struct {
|
||||
Branch []string `json:"branch"`
|
||||
Header string `json:"header"`
|
||||
Root string `json:"root"`
|
||||
}
|
||||
|
||||
// GetBlockHeader returns the block header at a specific height.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-block-header
|
||||
func (s *Client) GetBlockHeader(ctx context.Context, height uint32, checkpointHeight ...uint32) (*GetBlockHeaderResult, error) {
|
||||
if checkpointHeight != nil && checkpointHeight[0] != 0 {
|
||||
if height > checkpointHeight[0] {
|
||||
return nil, ErrCheckpointHeight
|
||||
}
|
||||
|
||||
var resp GetBlockHeaderResp
|
||||
err := s.request(ctx, "blockchain.block.header", []interface{}{height, checkpointHeight[0]}, &resp)
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
var resp basicResp
|
||||
err := s.request(ctx, "blockchain.block.header", []interface{}{height, 0}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetBlockHeaderResult{
|
||||
Branch: nil,
|
||||
Header: resp.Result,
|
||||
Root: "",
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetBlockHeadersResp represents the response to GetBlockHeaders().
|
||||
type GetBlockHeadersResp struct {
|
||||
Result *GetBlockHeadersResult `json:"result"`
|
||||
}
|
||||
|
||||
// GetBlockHeadersResult represents the content of the result field in the response to GetBlockHeaders().
|
||||
type GetBlockHeadersResult struct {
|
||||
Count uint32 `json:"count"`
|
||||
Headers string `json:"hex"`
|
||||
Max uint32 `json:"max"`
|
||||
Branch []string `json:"branch,omitempty"`
|
||||
Root string `json:"root,omitempty"`
|
||||
}
|
||||
|
||||
// GetBlockHeaders return a concatenated chunk of block headers.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-block-headers
|
||||
func (s *Client) GetBlockHeaders(ctx context.Context, startHeight, count uint32,
|
||||
checkpointHeight ...uint32) (*GetBlockHeadersResult, error) {
|
||||
|
||||
var resp GetBlockHeadersResp
|
||||
var err error
|
||||
|
||||
if checkpointHeight != nil && checkpointHeight[0] != 0 {
|
||||
if (startHeight + (count - 1)) > checkpointHeight[0] {
|
||||
return nil, ErrCheckpointHeight
|
||||
}
|
||||
|
||||
err = s.request(ctx, "blockchain.block.headers", []interface{}{startHeight, count, checkpointHeight[0]}, &resp)
|
||||
} else {
|
||||
err = s.request(ctx, "blockchain.block.headers", []interface{}{startHeight, count, 0}, &resp)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
64
electrum/misc.go
Normal file
64
electrum/misc.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package electrum
|
||||
|
||||
import "context"
|
||||
|
||||
type basicResp struct {
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
// GetFeeResp represents the response to GetFee().
|
||||
type GetFeeResp struct {
|
||||
Result float32 `json:"result"`
|
||||
}
|
||||
|
||||
// GetFee returns the estimated transaction fee per kilobytes for a transaction
|
||||
// to be confirmed within a target number of blocks.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-estimatefee
|
||||
func (s *Client) GetFee(ctx context.Context, target uint32) (float32, error) {
|
||||
var resp GetFeeResp
|
||||
|
||||
err := s.request(ctx, "blockchain.estimatefee", []interface{}{target}, &resp)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// GetRelayFee returns the minimum fee a transaction must pay to be accepted into the
|
||||
// remote server memory pool.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-relayfee
|
||||
func (s *Client) GetRelayFee(ctx context.Context) (float32, error) {
|
||||
var resp GetFeeResp
|
||||
|
||||
err := s.request(ctx, "blockchain.relayfee", []interface{}{}, &resp)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// GetFeeHistogramResp represents the response to GetFee().
|
||||
type getFeeHistogramResp struct {
|
||||
Result [][2]uint64 `json:"result"`
|
||||
}
|
||||
|
||||
// GetFeeHistogram returns a histogram of the fee rates paid by transactions in the
|
||||
// memory pool, weighted by transacation size.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#mempool-get-fee-histogram
|
||||
func (s *Client) GetFeeHistogram(ctx context.Context) (map[uint32]uint64, error) {
|
||||
var resp getFeeHistogramResp
|
||||
|
||||
err := s.request(ctx, "mempool.get_fee_histogram", []interface{}{}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
feeMap := make(map[uint32]uint64)
|
||||
for i := 0; i < len(resp.Result); i++ {
|
||||
feeMap[uint32(resp.Result[i][0])] = resp.Result[i][1]
|
||||
}
|
||||
|
||||
return feeMap, err
|
||||
}
|
278
electrum/network.go
Normal file
278
electrum/network.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package electrum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
// ClientVersion identifies the client version/name to the remote server
|
||||
ClientVersion = "go-electrum1.1"
|
||||
|
||||
// ProtocolVersion identifies the support protocol version to the remote server
|
||||
ProtocolVersion = "1.4"
|
||||
|
||||
nl = byte('\n')
|
||||
)
|
||||
|
||||
var (
|
||||
// DebugMode provides debug output on communications with the remote server if enabled.
|
||||
DebugMode bool
|
||||
|
||||
// ErrServerConnected throws an error if remote server is already connected.
|
||||
ErrServerConnected = errors.New("server is already connected")
|
||||
|
||||
// ErrServerShutdown throws an error if remote server has shutdown.
|
||||
ErrServerShutdown = errors.New("server has shutdown")
|
||||
|
||||
// ErrTimeout throws an error if request has timed out
|
||||
ErrTimeout = errors.New("request timeout")
|
||||
|
||||
// ErrNotImplemented throws an error if this RPC call has not been implemented yet.
|
||||
ErrNotImplemented = errors.New("RPC call is not implemented")
|
||||
|
||||
// ErrDeprecated throws an error if this RPC call is deprecated.
|
||||
ErrDeprecated = errors.New("RPC call has been deprecated")
|
||||
)
|
||||
|
||||
// Transport provides interface to server transport.
|
||||
type Transport interface {
|
||||
SendMessage([]byte) error
|
||||
Responses() <-chan []byte
|
||||
Errors() <-chan error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type container struct {
|
||||
content []byte
|
||||
err error
|
||||
}
|
||||
|
||||
// Client stores information about the remote server.
|
||||
type Client struct {
|
||||
transport Transport
|
||||
|
||||
handlers map[uint64]chan *container
|
||||
handlersLock sync.RWMutex
|
||||
|
||||
pushHandlers map[string][]chan *container
|
||||
pushHandlersLock sync.RWMutex
|
||||
|
||||
Error chan error
|
||||
quit chan struct{}
|
||||
|
||||
nextID uint64
|
||||
}
|
||||
|
||||
// NewClientTCP initialize a new client for remote server and connects to the remote server using TCP
|
||||
func NewClientTCP(ctx context.Context, addr string) (*Client, error) {
|
||||
transport, err := NewTCPTransport(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
handlers: make(map[uint64]chan *container),
|
||||
pushHandlers: make(map[string][]chan *container),
|
||||
|
||||
Error: make(chan error),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
|
||||
c.transport = transport
|
||||
go c.listen()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// NewClientSSL initialize a new client for remote server and connects to the remote server using SSL
|
||||
func NewClientSSL(ctx context.Context, addr string, config *tls.Config) (*Client, error) {
|
||||
transport, err := NewSSLTransport(ctx, addr, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
handlers: make(map[uint64]chan *container),
|
||||
pushHandlers: make(map[string][]chan *container),
|
||||
|
||||
Error: make(chan error),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
|
||||
c.transport = transport
|
||||
go c.listen()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type apiErr struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *apiErr) Error() string {
|
||||
return fmt.Sprintf("errNo: %d, errMsg: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
type response struct {
|
||||
ID uint64 `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (s *Client) listen() {
|
||||
for {
|
||||
if s.IsShutdown() {
|
||||
break
|
||||
}
|
||||
if s.transport == nil {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-s.quit:
|
||||
return
|
||||
case err := <-s.transport.Errors():
|
||||
s.Error <- err
|
||||
s.Shutdown()
|
||||
case bytes := <-s.transport.Responses():
|
||||
result := &container{
|
||||
content: bytes,
|
||||
}
|
||||
|
||||
msg := &response{}
|
||||
err := json.Unmarshal(bytes, msg)
|
||||
if err != nil {
|
||||
if DebugMode {
|
||||
log.Printf("Unmarshal received message failed: %v", err)
|
||||
}
|
||||
result.err = fmt.Errorf("Unmarshal received message failed: %v", err)
|
||||
} else if msg.Error != "" {
|
||||
result.err = errors.New(msg.Error)
|
||||
}
|
||||
|
||||
if len(msg.Method) > 0 {
|
||||
s.pushHandlersLock.RLock()
|
||||
handlers := s.pushHandlers[msg.Method]
|
||||
s.pushHandlersLock.RUnlock()
|
||||
|
||||
for _, handler := range handlers {
|
||||
select {
|
||||
case handler <- result:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.handlersLock.RLock()
|
||||
c, ok := s.handlers[msg.ID]
|
||||
s.handlersLock.RUnlock()
|
||||
|
||||
if ok {
|
||||
// TODO: very rare case. fix this memory leak, when nobody will read channel (in case of error)
|
||||
c <- result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Client) listenPush(method string) <-chan *container {
|
||||
c := make(chan *container, 1)
|
||||
s.pushHandlersLock.Lock()
|
||||
s.pushHandlers[method] = append(s.pushHandlers[method], c)
|
||||
s.pushHandlersLock.Unlock()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type request struct {
|
||||
ID uint64 `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params []interface{} `json:"params"`
|
||||
}
|
||||
|
||||
func (s *Client) request(ctx context.Context, method string, params []interface{}, v interface{}) error {
|
||||
select {
|
||||
case <-s.quit:
|
||||
return ErrServerShutdown
|
||||
default:
|
||||
}
|
||||
|
||||
msg := request{
|
||||
ID: atomic.AddUint64(&s.nextID, 1),
|
||||
Method: method,
|
||||
Params: params,
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytes = append(bytes, nl)
|
||||
|
||||
err = s.transport.SendMessage(bytes)
|
||||
if err != nil {
|
||||
s.Shutdown()
|
||||
return err
|
||||
}
|
||||
|
||||
c := make(chan *container, 1)
|
||||
|
||||
s.handlersLock.Lock()
|
||||
s.handlers[msg.ID] = c
|
||||
s.handlersLock.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.handlersLock.Lock()
|
||||
delete(s.handlers, msg.ID)
|
||||
s.handlersLock.Unlock()
|
||||
}()
|
||||
|
||||
var resp *container
|
||||
select {
|
||||
case resp = <-c:
|
||||
case <-ctx.Done():
|
||||
return ErrTimeout
|
||||
}
|
||||
|
||||
if resp.err != nil {
|
||||
return resp.err
|
||||
}
|
||||
|
||||
if v != nil {
|
||||
err = json.Unmarshal(resp.content, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Client) Shutdown() {
|
||||
if !s.IsShutdown() {
|
||||
close(s.quit)
|
||||
}
|
||||
if s.transport != nil {
|
||||
_ = s.transport.Close()
|
||||
}
|
||||
s.transport = nil
|
||||
s.handlers = nil
|
||||
s.pushHandlers = nil
|
||||
}
|
||||
|
||||
func (s *Client) IsShutdown() bool {
|
||||
select {
|
||||
case <-s.quit:
|
||||
return true
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
89
electrum/scripthash.go
Normal file
89
electrum/scripthash.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package electrum
|
||||
|
||||
import "context"
|
||||
|
||||
// GetBalanceResp represents the response to GetBalance().
|
||||
type GetBalanceResp struct {
|
||||
Result GetBalanceResult `json:"result"`
|
||||
}
|
||||
|
||||
// GetBalanceResult represents the content of the result field in the response to GetBalance().
|
||||
type GetBalanceResult struct {
|
||||
Confirmed float64 `json:"confirmed"`
|
||||
Unconfirmed float64 `json:"unconfirmed"`
|
||||
}
|
||||
|
||||
// GetBalance returns the confirmed and unconfirmed balance for a scripthash.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-balance
|
||||
func (s *Client) GetBalance(ctx context.Context, scripthash string) (GetBalanceResult, error) {
|
||||
var resp GetBalanceResp
|
||||
|
||||
err := s.request(ctx, "blockchain.scripthash.get_balance", []interface{}{scripthash}, &resp)
|
||||
if err != nil {
|
||||
return GetBalanceResult{}, err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// GetMempoolResp represents the response to GetHistory() and GetMempool().
|
||||
type GetMempoolResp struct {
|
||||
Result []*GetMempoolResult `json:"result"`
|
||||
}
|
||||
|
||||
// GetMempoolResult represents the content of the result field in the response
|
||||
// to GetHistory() and GetMempool().
|
||||
type GetMempoolResult struct {
|
||||
Hash string `json:"tx_hash"`
|
||||
Height int32 `json:"height"`
|
||||
Fee uint32 `json:"fee,omitempty"`
|
||||
}
|
||||
|
||||
// GetHistory returns the confirmed and unconfirmed history for a scripthash.
|
||||
func (s *Client) GetHistory(ctx context.Context, scripthash string) ([]*GetMempoolResult, error) {
|
||||
var resp GetMempoolResp
|
||||
|
||||
err := s.request(ctx, "blockchain.scripthash.get_history", []interface{}{scripthash}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// GetMempool returns the unconfirmed transacations of a scripthash.
|
||||
func (s *Client) GetMempool(ctx context.Context, scripthash string) ([]*GetMempoolResult, error) {
|
||||
var resp GetMempoolResp
|
||||
|
||||
err := s.request(ctx, "blockchain.scripthash.get_mempool", []interface{}{scripthash}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// ListUnspentResp represents the response to ListUnspent()
|
||||
type ListUnspentResp struct {
|
||||
Result []*ListUnspentResult `json:"result"`
|
||||
}
|
||||
|
||||
// ListUnspentResult represents the content of the result field in the response to ListUnspent()
|
||||
type ListUnspentResult struct {
|
||||
Height uint32 `json:"height"`
|
||||
Position uint32 `json:"tx_pos"`
|
||||
Hash string `json:"tx_hash"`
|
||||
Value uint64 `json:"value"`
|
||||
}
|
||||
|
||||
// ListUnspent returns an ordered list of UTXOs for a scripthash.
|
||||
func (s *Client) ListUnspent(ctx context.Context, scripthash string) ([]*ListUnspentResult, error) {
|
||||
var resp ListUnspentResp
|
||||
|
||||
err := s.request(ctx, "blockchain.scripthash.listunspent", []interface{}{scripthash}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
109
electrum/server.go
Normal file
109
electrum/server.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package electrum
|
||||
|
||||
import "context"
|
||||
|
||||
// Ping send a ping to the target server to ensure it is responding and
|
||||
// keeping the session alive.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-ping
|
||||
func (s *Client) Ping(ctx context.Context) error {
|
||||
err := s.request(ctx, "server.ping", []interface{}{}, nil)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ServerAddPeer adds your new server into the remote server own peers list.
|
||||
// This should not be used if you are a client.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-add-peer
|
||||
func (s *Client) ServerAddPeer(ctx context.Context, features *ServerFeaturesResult) error {
|
||||
var resp basicResp
|
||||
|
||||
err := s.request(ctx, "server.add_peer", []interface{}{features}, &resp)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ServerBanner returns the banner for this remote server.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-banner
|
||||
func (s *Client) ServerBanner(ctx context.Context) (string, error) {
|
||||
var resp basicResp
|
||||
|
||||
err := s.request(ctx, "server.banner", []interface{}{}, &resp)
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// ServerDonation returns the donation address for this remote server
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-donation-address
|
||||
func (s *Client) ServerDonation(ctx context.Context) (string, error) {
|
||||
var resp basicResp
|
||||
|
||||
err := s.request(ctx, "server.donation_address", []interface{}{}, &resp)
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
type host struct {
|
||||
TCPPort uint16 `json:"tcp_port,omitempty"`
|
||||
SSLPort uint16 `json:"ssl_port,omitempty"`
|
||||
}
|
||||
|
||||
// ServerFeaturesResp represent the response to GetFeatures().
|
||||
type ServerFeaturesResp struct {
|
||||
Result *ServerFeaturesResult `json:"result"`
|
||||
}
|
||||
|
||||
// ServerFeaturesResult represent the data sent or receive in RPC call "server.features" and
|
||||
// "server.add_peer".
|
||||
type ServerFeaturesResult struct {
|
||||
GenesisHash string `json:"genesis_hash"`
|
||||
Hosts map[string]host `json:"hosts"`
|
||||
ProtocolMax string `json:"protocol_max"`
|
||||
ProtocolMin string `json:"protocol_min"`
|
||||
Pruning bool `json:"pruning,omitempty"`
|
||||
ServerVersion string `json:"server_version"`
|
||||
HashFunction string `json:"hash_function"`
|
||||
}
|
||||
|
||||
// ServerFeatures returns a list of features and services supported by the remote server.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-features
|
||||
func (s *Client) ServerFeatures(ctx context.Context) (*ServerFeaturesResult, error) {
|
||||
var resp ServerFeaturesResp
|
||||
|
||||
err := s.request(ctx, "server.features", []interface{}{}, &resp)
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// ServerPeers returns a list of peers this remote server is aware of.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-peers-subscribe
|
||||
func (s *Client) ServerPeers(ctx context.Context) (interface{}, error) {
|
||||
resp := &struct {
|
||||
Result [][]interface{} `json:"result"`
|
||||
}{}
|
||||
err := s.request(ctx, "server.peers.subscribe", []interface{}{}, &resp)
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// ServerVersionResp represent the response to ServerVersion().
|
||||
type ServerVersionResp struct {
|
||||
Result [2]string `json:"result"`
|
||||
}
|
||||
|
||||
// ServerVersion identify the client to the server, and negotiate the protocol version.
|
||||
// This call must be sent first, or the server will default to an older protocol version.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-version
|
||||
func (s *Client) ServerVersion(ctx context.Context) (serverVer, protocolVer string, err error) {
|
||||
var resp ServerVersionResp
|
||||
|
||||
err = s.request(ctx, "server.version", []interface{}{ClientVersion, ProtocolVersion}, &resp)
|
||||
if err != nil {
|
||||
serverVer = ""
|
||||
protocolVer = ""
|
||||
} else {
|
||||
serverVer = resp.Result[0]
|
||||
protocolVer = resp.Result[1]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
250
electrum/subscribe.go
Normal file
250
electrum/subscribe.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package electrum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SubscribeHeadersResp represent the response to SubscribeHeaders().
|
||||
type SubscribeHeadersResp struct {
|
||||
Result *SubscribeHeadersResult `json:"result"`
|
||||
}
|
||||
|
||||
// SubscribeHeadersNotif represent the notification to SubscribeHeaders().
|
||||
type SubscribeHeadersNotif struct {
|
||||
Params []*SubscribeHeadersResult `json:"params"`
|
||||
}
|
||||
|
||||
// SubscribeHeadersResult represents the content of the result field in the response to SubscribeHeaders().
|
||||
type SubscribeHeadersResult struct {
|
||||
Height int32 `json:"height,omitempty"`
|
||||
Hex string `json:"hex"`
|
||||
}
|
||||
|
||||
// SubscribeHeaders subscribes to receive block headers notifications when new blocks are found.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
|
||||
func (s *Client) SubscribeHeaders(ctx context.Context) (<-chan *SubscribeHeadersResult, error) {
|
||||
var resp SubscribeHeadersResp
|
||||
|
||||
err := s.request(ctx, "blockchain.headers.subscribe", []interface{}{}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respChan := make(chan *SubscribeHeadersResult, 1)
|
||||
respChan <- resp.Result
|
||||
|
||||
go func() {
|
||||
for msg := range s.listenPush("blockchain.headers.subscribe") {
|
||||
if msg.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resp SubscribeHeadersNotif
|
||||
|
||||
err := json.Unmarshal(msg.content, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, param := range resp.Params {
|
||||
respChan <- param
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return respChan, nil
|
||||
}
|
||||
|
||||
// ScripthashSubscription ...
|
||||
type ScripthashSubscription struct {
|
||||
server *Client
|
||||
notifChan chan *SubscribeNotif
|
||||
|
||||
subscribedSH []string
|
||||
scripthashMap map[string]string
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// SubscribeNotif represent the notification to SubscribeScripthash() and SubscribeMasternode().
|
||||
type SubscribeNotif struct {
|
||||
Params [2]string `json:"params"`
|
||||
}
|
||||
|
||||
// SubscribeScripthash ...
|
||||
func (s *Client) SubscribeScripthash() (*ScripthashSubscription, <-chan *SubscribeNotif) {
|
||||
sub := &ScripthashSubscription{
|
||||
server: s,
|
||||
notifChan: make(chan *SubscribeNotif, 1),
|
||||
scripthashMap: make(map[string]string),
|
||||
}
|
||||
|
||||
go func() {
|
||||
for msg := range s.listenPush("blockchain.scripthash.subscribe") {
|
||||
if msg.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resp SubscribeNotif
|
||||
|
||||
err := json.Unmarshal(msg.content, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sub.lock.Lock()
|
||||
for _, a := range sub.subscribedSH {
|
||||
if a == resp.Params[0] {
|
||||
sub.notifChan <- &resp
|
||||
break
|
||||
}
|
||||
}
|
||||
sub.lock.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
return sub, sub.notifChan
|
||||
}
|
||||
|
||||
// Add ...
|
||||
func (sub *ScripthashSubscription) Add(ctx context.Context, scripthash string, address ...string) error {
|
||||
var resp basicResp
|
||||
|
||||
err := sub.server.request(ctx, "blockchain.scripthash.subscribe", []interface{}{scripthash}, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(resp.Result) > 0 {
|
||||
sub.notifChan <- &SubscribeNotif{[2]string{scripthash, resp.Result}}
|
||||
}
|
||||
|
||||
sub.lock.Lock()
|
||||
sub.subscribedSH = append(sub.subscribedSH[:], scripthash)
|
||||
if len(address) > 0 {
|
||||
sub.scripthashMap[scripthash] = address[0]
|
||||
}
|
||||
sub.lock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAddress ...
|
||||
func (sub *ScripthashSubscription) GetAddress(scripthash string) (string, error) {
|
||||
address, ok := sub.scripthashMap[scripthash]
|
||||
if ok {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
return "", errors.New("scripthash not found in map")
|
||||
}
|
||||
|
||||
// GetScripthash ...
|
||||
func (sub *ScripthashSubscription) GetScripthash(address string) (string, error) {
|
||||
var found bool
|
||||
var scripthash string
|
||||
|
||||
for k, v := range sub.scripthashMap {
|
||||
if v == address {
|
||||
scripthash = k
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
return scripthash, nil
|
||||
}
|
||||
|
||||
return "", errors.New("address not found in map")
|
||||
}
|
||||
|
||||
// GetChannel ...
|
||||
func (sub *ScripthashSubscription) GetChannel() <-chan *SubscribeNotif {
|
||||
return sub.notifChan
|
||||
}
|
||||
|
||||
// Remove ...
|
||||
func (sub *ScripthashSubscription) Remove(scripthash string) error {
|
||||
for i, v := range sub.subscribedSH {
|
||||
if v == scripthash {
|
||||
sub.lock.Lock()
|
||||
sub.subscribedSH = append(sub.subscribedSH[:i], sub.subscribedSH[i+1:]...)
|
||||
sub.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("scripthash not found")
|
||||
}
|
||||
|
||||
// RemoveAddress ...
|
||||
func (sub *ScripthashSubscription) RemoveAddress(address string) error {
|
||||
scripthash, err := sub.GetScripthash(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, v := range sub.subscribedSH {
|
||||
if v == scripthash {
|
||||
sub.lock.Lock()
|
||||
sub.subscribedSH = append(sub.subscribedSH[:i], sub.subscribedSH[i+1:]...)
|
||||
delete(sub.scripthashMap, scripthash)
|
||||
sub.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("scripthash not found")
|
||||
}
|
||||
|
||||
// Resubscribe ...
|
||||
func (sub *ScripthashSubscription) Resubscribe(ctx context.Context) error {
|
||||
for _, v := range sub.subscribedSH {
|
||||
err := sub.Add(ctx, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubscribeMasternode subscribes to receive notifications when a masternode status changes.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
|
||||
func (s *Client) SubscribeMasternode(ctx context.Context, collateral string) (<-chan string, error) {
|
||||
var resp basicResp
|
||||
|
||||
err := s.request(ctx, "blockchain.masternode.subscribe", []interface{}{collateral}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respChan := make(chan string, 1)
|
||||
if len(resp.Result) > 0 {
|
||||
respChan <- resp.Result
|
||||
}
|
||||
|
||||
go func() {
|
||||
for msg := range s.listenPush("blockchain.masternode.subscribe") {
|
||||
if msg.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resp SubscribeNotif
|
||||
|
||||
err := json.Unmarshal(msg.content, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, param := range resp.Params {
|
||||
respChan <- param
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return respChan, nil
|
||||
}
|
157
electrum/transaction.go
Normal file
157
electrum/transaction.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package electrum
|
||||
|
||||
import "context"
|
||||
|
||||
// BroadcastTransaction sends a raw transaction to the remote server to
|
||||
// be broadcasted on the server network.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-broadcast
|
||||
func (s *Client) BroadcastTransaction(ctx context.Context, rawTx string) (string, error) {
|
||||
resp := &basicResp{}
|
||||
err := s.request(ctx, "blockchain.transaction.broadcast", []interface{}{rawTx}, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// GetTransactionResp represents the response to GetTransaction().
|
||||
type GetTransactionResp struct {
|
||||
Result *GetTransactionResult `json:"result"`
|
||||
}
|
||||
|
||||
// GetTransactionResult represents the content of the result field in the response to GetTransaction().
|
||||
type GetTransactionResult struct {
|
||||
Blockhash string `json:"blockhash"`
|
||||
Blocktime uint64 `json:"blocktime"`
|
||||
Confirmations int32 `json:"confirmations"`
|
||||
Hash string `json:"hash"`
|
||||
Hex string `json:"hex"`
|
||||
Locktime uint32 `json:"locktime"`
|
||||
Size uint32 `json:"size"`
|
||||
Time uint64 `json:"time"`
|
||||
Version uint32 `json:"version"`
|
||||
Vin []Vin `json:"vin"`
|
||||
Vout []Vout `json:"vout"`
|
||||
Merkle GetMerkleProofResult `json:"merkle,omitempty"` // For protocol v1.5 and up.
|
||||
}
|
||||
|
||||
// Vin represents the input side of a transaction.
|
||||
type Vin struct {
|
||||
Coinbase string `json:"coinbase"`
|
||||
ScriptSig *ScriptSig `json:"scriptsig"`
|
||||
Sequence uint32 `json:"sequence"`
|
||||
TxID string `json:"txid"`
|
||||
Vout uint32 `json:"vout"`
|
||||
}
|
||||
|
||||
// ScriptSig represents the signature script for that transaction input.
|
||||
type ScriptSig struct {
|
||||
Asm string `json:"asm"`
|
||||
Hex string `json:"hex"`
|
||||
}
|
||||
|
||||
// Vout represents the output side of a transaction.
|
||||
type Vout struct {
|
||||
N uint32 `json:"n"`
|
||||
ScriptPubkey ScriptPubkey `json:"scriptpubkey"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
// ScriptPubkey represents the script of that transaction output.
|
||||
type ScriptPubkey struct {
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
Asm string `json:"asm"`
|
||||
Hex string `json:"hex,omitempty"`
|
||||
ReqSigs uint32 `json:"reqsigs,omitempty"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// GetTransaction gets the detailed information for a transaction.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get
|
||||
func (s *Client) GetTransaction(ctx context.Context, txHash string) (*GetTransactionResult, error) {
|
||||
var resp GetTransactionResp
|
||||
|
||||
err := s.request(ctx, "blockchain.transaction.get", []interface{}{txHash, true}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// GetRawTransaction gets a raw encoded transaction.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get
|
||||
func (s *Client) GetRawTransaction(ctx context.Context, txHash string) (string, error) {
|
||||
var resp basicResp
|
||||
|
||||
err := s.request(ctx, "blockchain.transaction.get", []interface{}{txHash, false}, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// GetMerkleProofResp represents the response to GetMerkleProof().
|
||||
type GetMerkleProofResp struct {
|
||||
Result *GetMerkleProofResult `json:"result"`
|
||||
}
|
||||
|
||||
// GetMerkleProofResult represents the content of the result field in the response to GetMerkleProof().
|
||||
type GetMerkleProofResult struct {
|
||||
Merkle []string `json:"merkle"`
|
||||
Height uint32 `json:"block_height"`
|
||||
Position uint32 `json:"pos"`
|
||||
}
|
||||
|
||||
// GetMerkleProof returns the merkle proof for a confirmed transaction.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get-merkle
|
||||
func (s *Client) GetMerkleProof(ctx context.Context, txHash string, height uint32) (*GetMerkleProofResult, error) {
|
||||
var resp GetMerkleProofResp
|
||||
|
||||
err := s.request(ctx, "blockchain.transaction.get_merkle", []interface{}{txHash, height}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// GetHashFromPosition returns the transaction hash for a specific position in a block.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-id-from-pos
|
||||
func (s *Client) GetHashFromPosition(ctx context.Context, height, position uint32) (string, error) {
|
||||
var resp basicResp
|
||||
|
||||
err := s.request(ctx, "blockchain.transaction.id_from_pos", []interface{}{height, position, false}, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// GetMerkleProofFromPosResp represents the response to GetMerkleProofFromPosition().
|
||||
type GetMerkleProofFromPosResp struct {
|
||||
Result *GetMerkleProofFromPosResult `json:"result"`
|
||||
}
|
||||
|
||||
// GetMerkleProofFromPosResult represents the content of the result field in the response
|
||||
// to GetMerkleProofFromPosition().
|
||||
type GetMerkleProofFromPosResult struct {
|
||||
Hash string `json:"tx_hash"`
|
||||
Merkle []string `json:"merkle"`
|
||||
}
|
||||
|
||||
// GetMerkleProofFromPosition returns the merkle proof for a specific position in a block.
|
||||
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-id-from-pos
|
||||
func (s *Client) GetMerkleProofFromPosition(ctx context.Context, height, position uint32) (*GetMerkleProofFromPosResult, error) {
|
||||
var resp GetMerkleProofFromPosResp
|
||||
|
||||
err := s.request(ctx, "blockchain.transaction.id_from_pos", []interface{}{height, position, true}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Result, err
|
||||
}
|
101
electrum/transport.go
Normal file
101
electrum/transport.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package electrum
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TCPTransport store information about the TCP transport.
|
||||
type TCPTransport struct {
|
||||
conn net.Conn
|
||||
responses chan []byte
|
||||
errors chan error
|
||||
}
|
||||
|
||||
// NewTCPTransport opens a new TCP connection to the remote server.
|
||||
func NewTCPTransport(ctx context.Context, addr string) (*TCPTransport, error) {
|
||||
var d net.Dialer
|
||||
|
||||
conn, err := d.DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tcp := &TCPTransport{
|
||||
conn: conn,
|
||||
responses: make(chan []byte),
|
||||
errors: make(chan error),
|
||||
}
|
||||
|
||||
go tcp.listen()
|
||||
|
||||
return tcp, nil
|
||||
}
|
||||
|
||||
// NewSSLTransport opens a new SSL connection to the remote server.
|
||||
func NewSSLTransport(ctx context.Context, addr string, config *tls.Config) (*TCPTransport, error) {
|
||||
dialer := tls.Dialer{
|
||||
NetDialer: &net.Dialer{},
|
||||
Config: config,
|
||||
}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tcp := &TCPTransport{
|
||||
conn: conn,
|
||||
responses: make(chan []byte),
|
||||
errors: make(chan error),
|
||||
}
|
||||
|
||||
go tcp.listen()
|
||||
|
||||
return tcp, nil
|
||||
}
|
||||
|
||||
func (t *TCPTransport) listen() {
|
||||
defer t.conn.Close()
|
||||
reader := bufio.NewReader(t.conn)
|
||||
|
||||
for {
|
||||
line, err := reader.ReadBytes(nl)
|
||||
if err != nil {
|
||||
t.errors <- err
|
||||
break
|
||||
}
|
||||
if DebugMode {
|
||||
log.Printf("%s [debug] %s -> %s", time.Now().Format("2006-01-02 15:04:05"), t.conn.RemoteAddr(), line)
|
||||
}
|
||||
|
||||
t.responses <- line
|
||||
}
|
||||
}
|
||||
|
||||
// SendMessage sends a message to the remote server through the TCP transport.
|
||||
func (t *TCPTransport) SendMessage(body []byte) error {
|
||||
if DebugMode {
|
||||
log.Printf("%s [debug] %s <- %s", time.Now().Format("2006-01-02 15:04:05"), t.conn.RemoteAddr(), body)
|
||||
}
|
||||
|
||||
_, err := t.conn.Write(body)
|
||||
return err
|
||||
}
|
||||
|
||||
// Responses returns chan to TCP transport responses.
|
||||
func (t *TCPTransport) Responses() <-chan []byte {
|
||||
return t.responses
|
||||
}
|
||||
|
||||
// Errors returns chan to TCP transport errors.
|
||||
func (t *TCPTransport) Errors() <-chan error {
|
||||
return t.errors
|
||||
}
|
||||
|
||||
func (t *TCPTransport) Close() error {
|
||||
return t.conn.Close()
|
||||
}
|
32
example/singleserver.go
Normal file
32
example/singleserver.go
Normal 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
23
go.mod
Normal 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
127
go.sum
Normal 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
BIN
media/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
Reference in New Issue
Block a user