Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
34a1029
Add txid to GetTransactionResult struct
Feb 17, 2023
c798e83
Updated example import
Feb 17, 2023
221d51b
Handle error string returned in the JSON-RPC response
nkuba May 16, 2023
447e9df
Add more context in unmarshall failure debug message
nkuba May 16, 2023
94663e5
Remove commented code
nkuba May 16, 2023
567f2c1
Merge pull request #1 from keep-network/json-rpc-error-unmarshal
lukasz-zimnoch May 16, 2023
17ac6f2
Support WebSocket protocol
nkuba May 19, 2023
5d10dfb
Expose generic client initialization function
nkuba May 19, 2023
d5f2ae3
Wait for close message response
nkuba May 23, 2023
befe891
Merge pull request #2 from keep-network/websocket
lukasz-zimnoch May 24, 2023
12c6646
Pass protocolVersion to ServerVersion function.
steveguo2triplea Aug 31, 2023
93cd0c3
Update go.mod and README.
steveguo2triplea Aug 31, 2023
384105d
Update example/singleserver.go.
steveguo2triplea Aug 31, 2023
5a63e0b
Fix json fields case issue.
steveguo2triplea Aug 31, 2023
f1de4c1
Try to fix transaction Unmashal issue.
steveguo2triplea Aug 31, 2023
1468c60
Add field `address` to scriptPubKey in Vout.
steveguo2triplea Aug 31, 2023
71d0cb9
Fix unmarshal error for Error object.
steveguo2triplea Aug 31, 2023
3f6013f
Change back to use *GetTransactionResult.
steveguo2triplea Sep 1, 2023
4d0439b
Change Height to int64.
steveguo2triplea Sep 2, 2023
d9baf77
Add WithTimeout DialerOption to transport.
steveguo2triplea Sep 8, 2023
c0d862e
Add functions to detail transaction and scripthash histories.
steveguo2triplea Oct 12, 2023
b8eccb0
Add function set totalSent totalReceived
steveguo2triplea Oct 12, 2023
7853034
Fix issue sum up total sent and total received for address
steveguo2triplea Oct 12, 2023
b95fccc
remove Printf from logger
steveguo2triplea Oct 12, 2023
7b7eb2c
Remove debug log
steveguo2triplea Oct 12, 2023
ebf8575
Fix typo.
steveguo2triplea Nov 30, 2023
5b9ed4d
Change logs to debug level
steveguo2triplea Dec 1, 2023
cd9fb60
Use sqlite db to cache tx instead of RAM.
steveguo2triplea Dec 2, 2023
cb937bd
Add lock to sqlite operations.
steveguo2triplea Dec 4, 2023
38108e4
Optmize the lock in TxCache.Load.
steveguo2triplea Dec 4, 2023
998ec80
Add transportLock to electrum client
steveguo2triplea Dec 12, 2023
e1a37d8
Add lock to more places.
steveguo2triplea Dec 12, 2023
3d87bea
Fix the locking issue.
steveguo2triplea Dec 12, 2023
23212fa
Remove transportLock but add flag to close transport.
steveguo2triplea Dec 12, 2023
a7a77fd
Fix data race issue.
steveguo2triplea Dec 12, 2023
5513656
Fix deadlock.
steveguo2triplea Dec 12, 2023
c9893a4
Add Mutex in TCPTransport.
steveguo2triplea Dec 12, 2023
80d28b8
Optimize locks.
steveguo2triplea Dec 12, 2023
f4e0ed2
Merge pull request #1 from triple-a/hotfix/data-race
steveguo2triplea Dec 12, 2023
6fd0c42
Fix the data race.
steveguo2triplea Dec 13, 2023
ad5e3ac
Expose the `SubscribeHeadersSingle` function
lukasz-zimnoch Feb 6, 2024
6038cb5
Merge pull request #5 from keep-network/expose-subscribe-headers-single
tomaszslabon Feb 6, 2024
1b6b4ca
Update dependencies
May 17, 2024
f76fcbf
Merge remote-tracking branch 'keep-network/master'
May 18, 2024
ecee726
Merge remote-tracking branch 'jpcummins/master'
May 19, 2024
12bc140
Revert "Merge remote-tracking branch 'jpcummins/master'"
May 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 82 additions & 6 deletions electrum/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"log"
"net/url"
"sync"
"sync/atomic"
)
Expand Down Expand Up @@ -70,6 +71,28 @@ type Client struct {
nextID uint64
}

// NewClient initializes a new client for remote server and connects to it using
// a transport protocol resolved from the URL's protocol scheme.
// A remote server URL should be provided in the `scheme://hostname:port` format
// (e.g. `tcp://electrum.io:50001`).
func NewClient(ctx context.Context, urlStr string, tlsConfig *tls.Config) (*Client, error) {
u, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("failed to parse url [%s]: [%w]", urlStr, err)
}

switch u.Scheme {
case "tcp":
return NewClientTCP(ctx, u.Host)
case "ssl":
return NewClientSSL(ctx, u.Host, tlsConfig)
case "ws", "wss":
return NewClientWebSocket(ctx, u.String(), tlsConfig)
}

return nil, fmt.Errorf("unsupported protocol scheme: [%s]", u.Scheme)
}

// 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)
Expand Down Expand Up @@ -112,6 +135,30 @@ func NewClientSSL(ctx context.Context, addr string, config *tls.Config) (*Client
return c, nil
}

// NewClientWebSocket initialize a new client for remote server and connects to
// the remote server using WebSocket.
func NewClientWebSocket(ctx context.Context, url string, config *tls.Config) (*Client, error) {
transport, err := NewWebSocketTransport(ctx, url, 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
}

// JSON-RPC 2.0 Error Object
// See: https://www.jsonrpc.org/specificationJSON#error_object
type apiErr struct {
Code int `json:"code"`
Message string `json:"message"`
Expand All @@ -121,10 +168,39 @@ func (e *apiErr) Error() string {
return fmt.Sprintf("errNo: %d, errMsg: %s", e.Code, e.Message)
}

// UnmarshalJSON defines a workaround for servers that respond with error
// that doesn't follow the JSON-RPC 2.0 Error Object format, i.e. electrs/esplora.
// See: https://github.com/Blockstream/esplora/issues/453
func (e *apiErr) UnmarshalJSON(data []byte) error {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf("failed to unmarshal error [%s]: %v", data, err)
}

switch v := v.(type) {
case string:
e.Message = v
case map[string]interface{}:
if _, ok := v["code"]; ok {
e.Code = int(v["code"].(float64))
}

if _, ok := v["message"]; ok {
e.Message = fmt.Sprint(v["message"])
}
default:
return fmt.Errorf("unsupported type: %v", v)
}

return nil
}

// JSON-RPC 2.0 Response Object
// See: https://www.jsonrpc.org/specification#response_object
type response struct {
ID uint64 `json:"id"`
Method string `json:"method"`
Error string `json:"error"`
ID uint64 `json:"id"`
Method string `json:"method"`
Error *apiErr `json:"error"`
}

func (s *Client) listen() {
Expand All @@ -150,11 +226,11 @@ func (s *Client) listen() {
err := json.Unmarshal(bytes, msg)
if err != nil {
if DebugMode {
log.Printf("Unmarshal received message failed: %v", err)
log.Printf("unmarshal received message [%s] failed: [%v]", bytes, err)
}
result.err = fmt.Errorf("Unmarshal received message failed: %v", err)
} else if msg.Error != "" {
result.err = errors.New(msg.Error)
} else if msg.Error != nil {
result.err = msg.Error
}

if len(msg.Method) > 0 {
Expand Down
36 changes: 36 additions & 0 deletions electrum/subscribe.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ type SubscribeHeadersResult struct {
}

// SubscribeHeaders subscribes to receive block headers notifications when new blocks are found.
//
// BEWARE: This function can lead to a memory leak if the caller stops
// pulling from the returned channel. See the SubscribeHeadersSingle method
// for a safer alternative.
//
// 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
Expand Down Expand Up @@ -58,6 +63,37 @@ func (s *Client) SubscribeHeaders(ctx context.Context) (<-chan *SubscribeHeaders
return respChan, nil
}

// SubscribeHeadersSingle subscribes to receive the header of the current
// blockchain tip. Unlike SubscribeHeaders, this method only returns the tip
// and does not listen for new block headers.
//
// Worth noting that this action still creates a new subscription in the Electrum
// server. The protocol does neither support a single-shot request for the
// current blockchain tip nor subscription cancellation. Although this limitation
// causes a slight resource overhead on the client, it does not cause a memory
// leak like the SubscribeHeaders method which spawns a goroutine that may hang
// on the channel if the caller is no longer pulling from it.
//
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
func (s *Client) SubscribeHeadersSingle(ctx context.Context) (
*SubscribeHeadersResult,
error,
) {
var resp SubscribeHeadersResp

err := s.request(
ctx,
"blockchain.headers.subscribe",
[]interface{}{},
&resp,
)
if err != nil {
return nil, err
}

return resp.Result, nil
}

// ScripthashSubscription ...
type ScripthashSubscription struct {
server *Client
Expand Down
1 change: 1 addition & 0 deletions electrum/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type GetTransactionResult struct {
Locktime uint32 `json:"locktime"`
Size uint32 `json:"size"`
Time uint64 `json:"time"`
Txid string `json:"txid"`
Version uint32 `json:"version"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
Expand Down
120 changes: 120 additions & 0 deletions electrum/transport_ws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package electrum

import (
"context"
"crypto/tls"
"log"
"time"

"github.com/gorilla/websocket"
)

type WebSocketTransport struct {
conn *websocket.Conn
responses chan []byte
errors chan error
// close is a channel used for graceful connection closure
close chan struct{}
}

const webSocketClosingTimeout = 2 * time.Second

// NewWebSocketTransport initializes new WebSocket transport.
func NewWebSocketTransport(
ctx context.Context,
url string,
tlsConfig *tls.Config,
) (*WebSocketTransport, error) {
dialer := websocket.Dialer{
TLSClientConfig: tlsConfig,
}

conn, response, err := dialer.DialContext(ctx, url, nil)
if err != nil {
if DebugMode {
log.Printf(
"%s [debug] connect -> status: %v, error: %v",
time.Now().Format("2006-01-02 15:04:05"),
response.Status,
err,
)
}
return nil, err
}

ws := &WebSocketTransport{
conn: conn,
responses: make(chan []byte),
errors: make(chan error),
close: make(chan struct{}),
}

go ws.listen()

return ws, nil
}

func (t *WebSocketTransport) listen() {
defer t.conn.Close()
defer close(t.close)

for {
_, msg, err := t.conn.ReadMessage()
if DebugMode {
log.Printf(
"%s [debug] %s -> msg: %s, err: %v",
time.Now().Format("2006-01-02 15:04:05"),
t.conn.RemoteAddr(),
msg,
err,
)
}
if err != nil {
isNormalClose := websocket.IsCloseError(err, websocket.CloseNormalClosure)
if !isNormalClose {
t.errors <- err
}

break
}

t.responses <- msg
}
}

// SendMessage sends a message to the remote server through the WebSocket transport.
func (t *WebSocketTransport) 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)
}

return t.conn.WriteMessage(websocket.TextMessage, body)
}

// Responses returns chan to WebSocket transport responses.
func (t *WebSocketTransport) Responses() <-chan []byte {
return t.responses
}

// Errors returns chan to WebSocket transport errors.
func (t *WebSocketTransport) Errors() <-chan error {
return t.errors
}

// Close closes WebSocket transport.
func (t *WebSocketTransport) Close() error {
// Cleanly close the connection by sending a close message and then
// waiting (with timeout) for the server to close the connection.
err := t.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Printf("%s [error] %s -> close error: %s", time.Now().Format("2006-01-02 15:04:05"), t.conn.RemoteAddr(), err)
}

select {
case <-t.close:
case <-time.After(webSocketClosingTimeout):
return t.conn.Close()
}

return nil
}
2 changes: 1 addition & 1 deletion example/singleserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"log"
"time"

"github.com/checksum0/go-electrum/electrum"
"github.com/jpcummins/go-electrum/electrum"
)

func main() {
Expand Down
23 changes: 12 additions & 11 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
module github.com/checksum0/go-electrum
module github.com/jpcummins/go-electrum

go 1.18

require (
github.com/btcsuite/btcd v0.23.1
github.com/btcsuite/btcd/btcutil v1.1.1
github.com/stretchr/testify v1.7.0
github.com/btcsuite/btcd v0.24.0
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/gorilla/websocket v1.5.0
github.com/stretchr/testify v1.8.0
)

require (
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/sys v0.20.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading