-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #37 from xyield/xrpl-client-wip
Xrpl client wip
- Loading branch information
Showing
19 changed files
with
1,102 additions
and
5 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,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 | ||
} |
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,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) | ||
} | ||
|
||
}) | ||
} | ||
} |
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,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}, | ||
} | ||
} |
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,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 | ||
} |
Oops, something went wrong.