-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 8b811e9
Showing
16 changed files
with
1,252 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2023 Anurag | ||
|
||
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 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# xrpl-go: A Go client for the XRP Ledger | ||
[![Go Report Card](https://goreportcard.com/badge/github.com/xrpscan/xrpl-go)](https://goreportcard.com/report/github.com/xrpscan/xrpl-go) [![GoDoc](https://pkg.go.dev/badge/github.com/xrpscan/xrpl-go?status.svg)](https://pkg.go.dev/github.com/xrpscan/xrpl-go) | ||
|
||
## Motivation | ||
|
||
We use Go and XRPL websocket APIs a lot a XRPScan. Unfortunately, the state of | ||
the Go client libraries for XRPL at the time of publishing this package is not | ||
ideal. This is where `xrpl-go` comes in. It provides a low level API for interacting | ||
with [XRPL websocket interface](https://xrpl.org/http-websocket-apis.html). This | ||
library aims to mirror concepts of the official JavaScript/TypeScript library | ||
[xrpl.js](https://github.com/XRPLF/xrpl.js). | ||
|
||
## Reference documentation | ||
|
||
See the [full reference documentation](https://pkg.go.dev/github.com/xrpscan/xrpl-go) | ||
for all packages, functions and constants. | ||
|
||
## Features | ||
|
||
1. Sending requests to observe ledger state using public websocket API methods | ||
2. Subscribing to changes in the ledger (ledger, transactions, validations streams) | ||
3. Parsing ledger data into mode convenient formats [WIP] | ||
|
||
## rippled versions | ||
|
||
`xrpl-go` is currently tested with rippled versions > 1.9.4. While it should | ||
also be compatible with later versions, newer features available on XRPL mainnet | ||
may not be available on day 0. | ||
|
||
## Installation | ||
|
||
```bash | ||
go get -u github.com/xrpscan/xrpl-go | ||
``` | ||
|
||
## Getting started | ||
|
||
Here are some examples showing typical use: | ||
|
||
#### Establish a new websocket connection | ||
```go | ||
config := xrpl.ClientConfig{ | ||
URL: "wss://s.altnet.rippletest.net:51233", | ||
} | ||
client := xrpl.NewClient(config) | ||
err := client.Ping([]byte("PING")) | ||
if err != nil { | ||
panic(err) | ||
} | ||
``` | ||
|
||
#### Send `account_info` request | ||
```go | ||
request := xrpl.BaseRequest{ | ||
"command": "account_info", | ||
"account": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", | ||
"ledger_index": "validated", | ||
} | ||
response, err := client.Request(request) | ||
if err != nil { | ||
fmt.Println(err) | ||
} | ||
fmt.Println(response) | ||
``` | ||
|
||
#### Subscribe to a single stream | ||
```go | ||
client.Subscribe([]string{ | ||
xrpl.StreamTypeLedger, | ||
}) | ||
for { | ||
ledger := <-client.StreamLedger | ||
fmt.Println(string(ledger)) | ||
} | ||
``` | ||
|
||
#### Subscribe to multiple streams | ||
```go | ||
client.Subscribe([]string{ | ||
xrpl.StreamTypeLedger, | ||
xrpl.StreamTypeTransaction, | ||
xrpl.StreamTypeValidations, | ||
}) | ||
for { | ||
select { | ||
case ledger := <-client.StreamLedger: | ||
fmt.Println(string(ledger)) | ||
case transaction := <-client.StreamTransaction: | ||
fmt.Println(string(transaction)) | ||
case validation := <-client.StreamValidation: | ||
fmt.Println(string(validation)) | ||
} | ||
} | ||
``` | ||
|
||
## Bugs | ||
|
||
`xrpl-go` is a work in progress. If you discover a bug or come across erratic | ||
behavior, please [create an issue](https://github.com/xrpscan/xrpl-go/issues/new) | ||
and we'll do our best to address it. | ||
|
||
## References | ||
|
||
- [XRPL HTTP/WebSocket API methods](https://xrpl.org/public-api-methods.html) | ||
- [XRPL WebSocket streams](https://xrpl.org/subscribe.html) | ||
- [JavaScript/TypeScript library for interacting with the XRP Ledger](https://js.xrpl.org) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
### Security Policy | ||
|
||
Found a security issue in a XRPScan project? Read on. | ||
|
||
### Reporting a Vulnerability | ||
|
||
Vulnerabilities should be reported to support@xrpscan.com - which is a private ticketing system monitored by the project maintainers. We will attempt to respond to/confirm reports within 2-3 days, but if you believe your report to be "critical" to user safety and security, please note as such in the subject. We have tens of thousands of users using our services, and take security vulnerabilities seriously. | ||
|
||
When reporting an issue, where possible, please provide at least: | ||
|
||
- The project and commit version the issue was identified at | ||
- A proof of concept (plaintext; no binaries) | ||
- Steps to reproduce | ||
- Your recommended remediation(s), if any. | ||
|
||
The project maintainers may reach back out for clarification. | ||
|
||
Note: Please do not open public issues for security issues, as GitHub does not provide facility for private issues, and deleting the issue makes it hard to triage/respond back to the reporter. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
package xrpl | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"log" | ||
"math" | ||
"net/http" | ||
"strconv" | ||
"sync" | ||
"time" | ||
|
||
"github.com/gorilla/websocket" | ||
) | ||
|
||
type ClientConfig struct { | ||
URL string | ||
Authorization string | ||
Certificate string | ||
ConnectionTimeout time.Duration | ||
FeeCushion uint32 | ||
Key string | ||
MaxFeeXRP uint64 | ||
Passphrase byte | ||
Proxy byte | ||
ProxyAuthorization byte | ||
Timeout time.Duration | ||
QueueCapacity int | ||
} | ||
|
||
type Client struct { | ||
config ClientConfig | ||
connection *websocket.Conn | ||
closed bool | ||
mutex sync.Mutex | ||
response *http.Response | ||
StreamLedger chan []byte | ||
StreamTransaction chan []byte | ||
StreamValidation chan []byte | ||
StreamManifest chan []byte | ||
StreamPeerStatus chan []byte | ||
StreamConsensus chan []byte | ||
StreamPathFind chan []byte | ||
StreamServer chan []byte | ||
StreamDefault chan []byte | ||
requestQueue map[string](chan<- BaseResponse) | ||
nextId int | ||
err error | ||
} | ||
|
||
func (config *ClientConfig) Validate() error { | ||
if len(config.URL) == 0 { | ||
return errors.New("cannot create a new connection with an empty URL") | ||
} | ||
|
||
if config.ConnectionTimeout < 0 || config.ConnectionTimeout >= math.MaxInt32 { | ||
return fmt.Errorf("connection timeout out of bounds: %d", config.ConnectionTimeout) | ||
} | ||
|
||
if config.Timeout < 0 || config.Timeout >= math.MaxInt32 { | ||
return fmt.Errorf("timeout out of bounds: %d", config.Timeout) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func NewClient(config ClientConfig) *Client { | ||
if err := config.Validate(); err != nil { | ||
panic(err) | ||
} | ||
|
||
if config.ConnectionTimeout == 0 { | ||
config.ConnectionTimeout = 60 * time.Second | ||
} | ||
|
||
if config.QueueCapacity == 0 { | ||
config.QueueCapacity = 128 | ||
} | ||
|
||
client := &Client{ | ||
config: config, | ||
StreamLedger: make(chan []byte, config.QueueCapacity), | ||
StreamTransaction: make(chan []byte, config.QueueCapacity), | ||
StreamValidation: make(chan []byte, config.QueueCapacity), | ||
StreamManifest: make(chan []byte, config.QueueCapacity), | ||
StreamPeerStatus: make(chan []byte, config.QueueCapacity), | ||
StreamConsensus: make(chan []byte, config.QueueCapacity), | ||
StreamPathFind: make(chan []byte, config.QueueCapacity), | ||
StreamServer: make(chan []byte, config.QueueCapacity), | ||
StreamDefault: make(chan []byte, config.QueueCapacity), | ||
requestQueue: make(map[string](chan<- BaseResponse)), | ||
nextId: 0, | ||
} | ||
c, r, err := websocket.DefaultDialer.Dial(config.URL, nil) | ||
if err != nil { | ||
client.err = err | ||
return nil | ||
} | ||
defer r.Body.Close() | ||
client.connection = c | ||
client.response = r | ||
client.connection.SetPongHandler(client.handlePong) | ||
go client.handleResponse() | ||
return client | ||
} | ||
|
||
func (c *Client) Ping(message []byte) error { | ||
if err := c.connection.WriteMessage(websocket.PingMessage, message); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// Returns incremental ID that may be used as request ID for websocket requests | ||
func (c *Client) NextID() string { | ||
c.mutex.Lock() | ||
c.nextId++ | ||
c.mutex.Unlock() | ||
return strconv.Itoa(c.nextId) | ||
} | ||
|
||
func (c *Client) Subscribe(streams []string) (BaseResponse, error) { | ||
req := BaseRequest{ | ||
"command": "subscribe", | ||
"streams": streams, | ||
} | ||
res, err := c.Request(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return res, nil | ||
} | ||
|
||
func (c *Client) Unsubscribe(streams []string) (BaseResponse, error) { | ||
req := BaseRequest{ | ||
"command": "unsubscribe", | ||
"streams": streams, | ||
} | ||
res, err := c.Request(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return res, nil | ||
} | ||
|
||
// Send a websocket request. This method takes a BaseRequest object and automatically adds | ||
// incremental request ID to it. | ||
// | ||
// Example usage: | ||
// | ||
// req := BaseRequest{ | ||
// "command": "account_info", | ||
// "account": "rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn", | ||
// "ledger_index": "current", | ||
// } | ||
// | ||
// err := client.Request(req, func(){}) | ||
func (c *Client) Request(req BaseRequest) (BaseResponse, error) { | ||
requestId := c.NextID() | ||
req["id"] = requestId | ||
data, err := json.Marshal(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
ch := make(chan BaseResponse, 1) | ||
|
||
c.mutex.Lock() | ||
c.requestQueue[requestId] = ch | ||
err = c.connection.WriteMessage(websocket.TextMessage, data) | ||
if err != nil { | ||
return nil, err | ||
} | ||
c.mutex.Unlock() | ||
|
||
res := <-ch | ||
return res, nil | ||
} | ||
|
||
func (c *Client) Close() error { | ||
c.mutex.Lock() | ||
defer c.mutex.Unlock() | ||
c.closed = true | ||
|
||
err := c.connection.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) | ||
if err != nil { | ||
log.Println("Write close error: ", err) | ||
return err | ||
} | ||
err = c.connection.Close() | ||
if err != nil { | ||
log.Println("Write close error: ", err) | ||
return err | ||
} | ||
return nil | ||
} |
Oops, something went wrong.