Skip to content

Commit

Permalink
Extracted xrpl-go client
Browse files Browse the repository at this point in the history
  • Loading branch information
pkcs8 committed Jun 6, 2023
0 parents commit 8b811e9
Show file tree
Hide file tree
Showing 16 changed files with 1,252 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE
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.
106 changes: 106 additions & 0 deletions README.md
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)
18 changes: 18 additions & 0 deletions SECURITY.md
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.
197 changes: 197 additions & 0 deletions client.go
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
}
Loading

0 comments on commit 8b811e9

Please sign in to comment.