Skip to content

Commit

Permalink
Merge pull request #37 from xyield/xrpl-client-wip
Browse files Browse the repository at this point in the history
Xrpl client wip
  • Loading branch information
bmurphy333 authored Aug 14, 2023
2 parents 22a122d + 4f3e49f commit 82ab9e6
Show file tree
Hide file tree
Showing 19 changed files with 1,102 additions and 5 deletions.
30 changes: 30 additions & 0 deletions client/account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package client

import (
"github.com/xyield/xrpl-go/model/client/account"
)

type Account interface {
// return result struct, fill xrpl response for warnings etc, error
GetAccountChannels(req *account.AccountChannelsRequest) (*account.AccountChannelsResponse, XRPLResponse, error)
}

type accountImpl struct {
Client Client
}

func (a *accountImpl) GetAccountChannels(req *account.AccountChannelsRequest) (*account.AccountChannelsResponse, XRPLResponse, error) {

result, err := a.Client.SendRequest(req)
if err != nil {
return nil, nil, err
}

var channelResponse account.AccountChannelsResponse
err = result.GetResult(&channelResponse)
if err != nil {
return nil, nil, err
}

return &channelResponse, result, nil
}
131 changes: 131 additions & 0 deletions client/account_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package client

import (
"errors"
"testing"

"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/xyield/xrpl-go/model/client/account"
"github.com/xyield/xrpl-go/model/client/common"
)

type mockClient struct {
mock.Mock
}

type mockClientXrplResponse struct {
Result map[string]any
}

func (m *mockClientXrplResponse) GetResult(v any) error {
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &v})
if err != nil {
return err
}
err = dec.Decode(m.Result)
if err != nil {
return err
}
return nil
}

func (m *mockClient) SendRequest(req XRPLRequest) (XRPLResponse, error) {
args := m.Called(req)
return args.Get(0).(XRPLResponse), args.Error(1)
}

func TestGetAccountChannels(t *testing.T) {

tt := []struct {
description string
input account.AccountChannelsRequest
sendRequestResult mockClientXrplResponse
output account.AccountChannelsResponse
expectedErr error
}{
{
description: "GetResult returns an error",
input: account.AccountChannelsRequest{
Account: "rLHmBn4fT92w4F6ViyYbjoizLTo83tHTHu",
DestinationAccount: "rnZvsWuLem5Ha46AZs61jLWR9R5esinkG3",
},
sendRequestResult: mockClientXrplResponse{
Result: map[string]any{
"account": 123,
"destination_account": "rnZvsWuLem5Ha46AZs61jLWR9R5esinkG3",
},
},
output: account.AccountChannelsResponse{},
expectedErr: errors.New("1 error(s) decoding:\n\n* 'account' expected type 'types.Address', got unconvertible type 'int', value: '123'"),
},
{
description: "successful response",
input: account.AccountChannelsRequest{
Account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
DestinationAccount: "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
LedgerIndex: common.VALIDATED,
},
sendRequestResult: mockClientXrplResponse{
Result: map[string]any{
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"channels": []any{
map[string]any{
"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"amount": "1000",
"balance": "0",
"channel_id": "C7F634794B79DB40E87179A9D1BF05D05797AE7E92DF8E93FD6656E8C4BE3AE7",
"destination_account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"public_key": "aBR7mdD75Ycs8DRhMgQ4EMUEmBArF8SEh1hfjrT2V9DQTLNbJVqw",
"public_key_hex": "03CFD18E689434F032A4E84C63E2A3A6472D684EAF4FD52CA67742F3E24BAE81B2",
"settle_delay": 60,
},
},
"ledger_hash": "1EDBBA3C793863366DF5B31C2174B6B5E6DF6DB89A7212B86838489148E2A581",
"ledger_index": 71766314,
"validated": true,
},
},
output: account.AccountChannelsResponse{
Account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
LedgerIndex: 71766314,
LedgerHash: "1EDBBA3C793863366DF5B31C2174B6B5E6DF6DB89A7212B86838489148E2A581",
Channels: []account.ChannelResult{
{
Account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
Amount: "1000",
Balance: "0",
ChannelID: "C7F634794B79DB40E87179A9D1BF05D05797AE7E92DF8E93FD6656E8C4BE3AE7",
DestinationAccount: "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
PublicKey: "aBR7mdD75Ycs8DRhMgQ4EMUEmBArF8SEh1hfjrT2V9DQTLNbJVqw",
PublicKeyHex: "03CFD18E689434F032A4E84C63E2A3A6472D684EAF4FD52CA67742F3E24BAE81B2",
SettleDelay: 60,
},
},
Validated: true,
},
expectedErr: nil,
},
}

for _, tc := range tt {

t.Run(tc.description, func(t *testing.T) {

cl := new(mockClient)
a := &accountImpl{Client: cl}

cl.On("SendRequest", &tc.input).Return(&tc.sendRequestResult, nil)

res, _, err := a.GetAccountChannels(&tc.input)

if tc.expectedErr != nil {
require.EqualError(t, err, tc.expectedErr.Error())
} else {
require.Equal(t, &tc.output, res)
}

})
}
}
30 changes: 30 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package client

type Client interface {
SendRequest(reqParams XRPLRequest) (XRPLResponse, error)
}

type XRPLClient struct {
Account Account
}

type XRPLRequest interface {
Method() string
Validate() error
}

type XRPLResponse interface {
GetResult(v any) error
}

type XRPLResponseWarning struct {
Id string `json:"id"`
Message string `json:"message"`
Details any `json:"details,omitempty"`
}

func NewXRPLClient(cl Client) *XRPLClient {
return &XRPLClient{
Account: &accountImpl{Client: cl},
}
}
196 changes: 196 additions & 0 deletions client/jsonrpc/jsonrpc_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package jsonrpcclient

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"

jsoniter "github.com/json-iterator/go"
"github.com/mitchellh/mapstructure"
"github.com/xyield/xrpl-go/client"
jsonrpcmodels "github.com/xyield/xrpl-go/client/jsonrpc/models"
)

type JsonRpcClient struct {
Config *client.JsonRpcConfig
}

type JsonRpcClientError struct {
ErrorString string
}

func (e *JsonRpcClientError) Error() string {
return e.ErrorString
}

var ErrIncorrectId = errors.New("incorrect id")

func NewJsonRpcClient(cfg *client.JsonRpcConfig) *JsonRpcClient {
return &JsonRpcClient{
Config: cfg,
}
}

func NewClient(cfg *client.JsonRpcConfig) *client.XRPLClient {
jc := &JsonRpcClient{
Config: cfg,
}
return client.NewXRPLClient(jc)
}

// satisfy the Client interface
func (c *JsonRpcClient) SendRequest(reqParams client.XRPLRequest) (client.XRPLResponse, error) {

err := reqParams.Validate()
if err != nil {
return nil, err
}

body, err := CreateRequest(reqParams)
if err != nil {
return nil, err
}

req, err := http.NewRequest(http.MethodPost, c.Config.Url, bytes.NewReader(body))
if err != nil {
return nil, err
}

// add timeout context to prevent hanging
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
req = req.WithContext(ctx)

req.Header = c.Config.Headers

var response *http.Response

response, err = c.Config.HTTPClient.Do(req)
if err != nil || response == nil {
return nil, err
}

// allow client to reuse persistant connection
defer response.Body.Close()

// Check for service unavailable response and retry if so
if response.StatusCode == 503 {

maxRetries := 3
backoffDuration := 1 * time.Second

for i := 0; i < maxRetries; i++ {
time.Sleep(backoffDuration)

// Make request again after waiting
response, err = c.Config.HTTPClient.Do(req)
if err != nil {
return nil, err
}

if response.StatusCode != 503 {
break
}

// Increase backoff duration for the next retry
backoffDuration *= 2
}

if response.StatusCode == 503 {
// Return service unavailable error here after retry 3 times
return nil, &JsonRpcClientError{ErrorString: "Server is overloaded, rate limit exceeded"}
}

}

var jr jsonrpcmodels.JsonRpcResponse
jr, err = CheckForError(response)
if err != nil {
return nil, err
}

return &jr, nil
}

// CreateRequest formats the parameters and method name ready for sending request
// Params will have been serialised if required and added to request struct before being passed to this method
func CreateRequest(reqParams client.XRPLRequest) ([]byte, error) {

var body jsonrpcmodels.JsonRpcRequest

body = jsonrpcmodels.JsonRpcRequest{
Method: reqParams.Method(),
// each param object will have a struct with json serialising tags
Params: [1]interface{}{reqParams},
}

// Omit the Params field if method doesn't require any
paramBytes, err := jsoniter.Marshal(body.Params)
if err != nil {
return nil, err
}
paramString := string(paramBytes)
if strings.Compare(paramString, "[{}]") == 0 {
// need to remove params field from the body if it is empty
body = jsonrpcmodels.JsonRpcRequest{
Method: reqParams.Method(),
}

jsonBytes, err := jsoniter.Marshal(body)
if err != nil {
return nil, err
}

return jsonBytes, nil
}

jsonBytes, err := jsoniter.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal JSON-RPC request for method %s with parameters %+v: %w", reqParams.Method(), reqParams, err)
}

return jsonBytes, nil
}

// CheckForError reads the http response and formats the error if it exists
func CheckForError(res *http.Response) (jsonrpcmodels.JsonRpcResponse, error) {

var jr jsonrpcmodels.JsonRpcResponse

b, err := ioutil.ReadAll(res.Body)
if err != nil || b == nil {
return jr, err
}

// In case a different error code is returned
if res.StatusCode != 200 {
return jr, &JsonRpcClientError{ErrorString: string(b)}
}

jDec := json.NewDecoder(bytes.NewReader(b))
jDec.UseNumber()
var m map[string]any
err = jDec.Decode(&m)
if err != nil {
return jr, err
}

dec, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &jr})
err = dec.Decode(&m)
if err != nil {
return jr, err
}

// result will have 'error' if error response
if _, ok := jr.Result["error"]; ok {
return jr, &JsonRpcClientError{ErrorString: jr.Result["error"].(string)}
}

return jr, nil
}
Loading

0 comments on commit 82ab9e6

Please sign in to comment.