Skip to content

Commit

Permalink
clients/horizonclient: Add support for Accounts endpoint. (#2229)
Browse files Browse the repository at this point in the history
* clients/horizonclient: Add support for Accounts endpoint.

Extend the horizonclient to support accounts filter. It allows you to
retrive all the accounts with a given signer or with a trustline to an
asset.

```go
	client := horizonclient.DefaultPublicNetClient
	accountsRequest := horizonclient.AccountsRequest{Signer: "GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU"}

	account, err := client.Accounts(accountsRequest)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Print(account)
```

* Apply suggestions from code review

Co-Authored-By: Eric Saunders <ire-and-curses@users.noreply.github.com>

* Remove if.

* Typos.

Co-authored-by: Eric Saunders <ire-and-curses@users.noreply.github.com>
  • Loading branch information
abuiles and ire-and-curses committed Apr 7, 2020
1 parent ed641b3 commit 50cae75
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 1 deletion.
56 changes: 56 additions & 0 deletions clients/horizonclient/accounts_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package horizonclient

import (
"fmt"
"net/url"

"github.com/stellar/go/support/errors"
)

// BuildURL creates the endpoint to be queried based on the data in the AccountsRequest struct.
// Either "Signer" or "Asset" fields should be set when retrieving Accounts.
// At the moment, you can't use both filters at the same time.
func (r AccountsRequest) BuildURL() (endpoint string, err error) {

nParams := countParams(r.Signer, r.Asset)

if nParams <= 0 {
err = errors.New("invalid request: no parameters - Signer or Asset must be provided")
}

if nParams >= 2 {
err = errors.New("invalid request: too many parameters - Signer and Asset provided, provide a single filter")
}

if err != nil {
return endpoint, err
}
query := url.Values{}
switch {
case len(r.Signer) > 0:
query.Add("signer", r.Signer)

case len(r.Asset) > 0:
query.Add("asset", r.Asset)
}

endpoint = fmt.Sprintf(
"accounts?%s",
query.Encode(),
)

if pageParams := addQueryParams(cursor(r.Cursor), limit(r.Limit), r.Order); len(pageParams) > 0 {
endpoint = fmt.Sprintf(
"%s&%s",
endpoint,
pageParams,
)
}

_, err = url.Parse(endpoint)
if err != nil {
err = errors.Wrap(err, "failed to parse endpoint")
}

return endpoint, err
}
8 changes: 8 additions & 0 deletions clients/horizonclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ func (c *Client) HorizonTimeOut() time.Duration {
return c.horizonTimeOut
}

// Accounts returns accounts who have a given signer or
// have a trustline to an asset.
// See https://www.stellar.org/developers/horizon/reference/endpoints/accounts.html
func (c *Client) Accounts(request AccountsRequest) (accounts hProtocol.AccountsPage, err error) {
err = c.sendRequest(request, &accounts)
return
}

// AccountDetail returns information for a single account.
// See https://www.stellar.org/developers/horizon/reference/endpoints/accounts-single.html
func (c *Client) AccountDetail(request AccountRequest) (account hProtocol.Account, err error) {
Expand Down
13 changes: 13 additions & 0 deletions clients/horizonclient/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ import (
"github.com/stellar/go/protocols/horizon/operations"
)

func ExampleClient_Accounts() {
client := horizonclient.DefaultPublicNetClient
accountsRequest := horizonclient.AccountsRequest{Signer: "GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU"}

account, err := client.Accounts(accountsRequest)
if err != nil {
fmt.Println(err)
return
}

fmt.Print(account)
}

func ExampleClient_AccountDetail() {
client := horizonclient.DefaultPublicNetClient
accountRequest := horizonclient.AccountRequest{AccountID: "GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU"}
Expand Down
14 changes: 13 additions & 1 deletion clients/horizonclient/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ type Client struct {

// ClientInterface contains methods implemented by the horizon client
type ClientInterface interface {
Accounts(request AccountsRequest) (hProtocol.AccountsPage, error)
AccountDetail(request AccountRequest) (hProtocol.Account, error)
AccountData(request AccountRequest) (hProtocol.AccountData, error)
Effects(request EffectRequest) (effects.EffectsPage, error)
Expand Down Expand Up @@ -212,7 +213,18 @@ type HorizonRequest interface {
BuildURL() (string, error)
}

// AccountRequest struct contains data for making requests to the accounts endpoint of a horizon server.
// AccountsRequest struct contains data for making requests to the accounts endpoint of a horizon server.
// Either "Signer" or "Asset" fields should be set when retrieving Accounts.
// At the moment, you can't use both filters at the same time.
type AccountsRequest struct {
Signer string
Asset string
Order Order
Cursor string
Limit uint
}

// AccountRequest struct contains data for making requests to the show account endpoint of a horizon server.
// "AccountID" and "DataKey" fields should both be set when retrieving AccountData.
// When getting the AccountDetail, only "AccountID" needs to be set.
type AccountRequest struct {
Expand Down
166 changes: 166 additions & 0 deletions clients/horizonclient/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,86 @@ func TestFixHTTP(t *testing.T) {
assert.IsType(t, client.HTTP, &http.Client{})
}

func TestAccounts(t *testing.T) {
tt := assert.New(t)
hmock := httptest.NewClient()
client := &Client{
HorizonURL: "https://localhost/",
HTTP: hmock,
}

accountRequest := AccountsRequest{}
_, err := client.Accounts(accountRequest)
if tt.Error(err) {
tt.Contains(err.Error(), "invalid request: no parameters - Signer or Asset must be provided")
}

accountRequest = AccountsRequest{
Signer: "GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU",
Asset: "COP:GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU",
}
_, err = client.Accounts(accountRequest)
if tt.Error(err) {
tt.Contains(err.Error(), "invalid request: too many parameters - Signer and Asset provided, provide a single filter")
}

var accounts hProtocol.AccountsPage

hmock.On(
"GET",
"https://localhost/accounts?signer=GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
).ReturnString(200, accountsResponse)

accountRequest = AccountsRequest{
Signer: "GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
}
accounts, err = client.Accounts(accountRequest)
tt.NoError(err)
tt.Len(accounts.Embedded.Records, 1)

hmock.On(
"GET",
"https://localhost/accounts?asset=COP%3AGAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
).ReturnString(200, accountsResponse)

accountRequest = AccountsRequest{
Asset: "COP:GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
}
accounts, err = client.Accounts(accountRequest)
tt.NoError(err)
tt.Len(accounts.Embedded.Records, 1)

hmock.On(
"GET",
"https://localhost/accounts?signer=GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP&cursor=GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H&limit=200&order=desc",
).ReturnString(200, accountsResponse)

accountRequest = AccountsRequest{
Signer: "GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
Order: "desc",
Cursor: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H",
Limit: 200,
}
accounts, err = client.Accounts(accountRequest)
tt.NoError(err)
tt.Len(accounts.Embedded.Records, 1)

// connection error
hmock.On(
"GET",
"https://localhost/accounts?signer=GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
).ReturnError("http.Client error")

accountRequest = AccountsRequest{
Signer: "GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
}
accounts, err = client.Accounts(accountRequest)
if tt.Error(err) {
tt.Contains(err.Error(), "http.Client error")
_, ok := err.(*Error)
tt.Equal(ok, false)
}
}
func TestAccountDetail(t *testing.T) {
hmock := httptest.NewClient()
client := &Client{
Expand Down Expand Up @@ -739,6 +819,92 @@ func TestFetchTimebounds(t *testing.T) {
assert.Equal(t, st.MaxTime, int64(200))
}

var accountsResponse = `{
"_links": {
"self": {
"href": "https://horizon-testnet.stellar.org/accounts?cursor=\u0026limit=10\u0026order=asc\u0026signer=GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP"
},
"next": {
"href": "https://horizon-testnet.stellar.org/accounts?cursor=GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP\u0026limit=10\u0026order=asc\u0026signer=GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP"
},
"prev": {
"href": "https://horizon-testnet.stellar.org/accounts?cursor=GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP\u0026limit=10\u0026order=desc\u0026signer=GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP"
}
},
"_embedded": {
"records": [
{
"_links": {
"self": {
"href": "https://horizon-testnet.stellar.org/accounts/GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP"
},
"transactions": {
"href": "https://horizon-testnet.stellar.org/accounts/GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP/transactions{?cursor,limit,order}",
"templated": true
},
"operations": {
"href": "https://horizon-testnet.stellar.org/accounts/GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP/operations{?cursor,limit,order}",
"templated": true
},
"payments": {
"href": "https://horizon-testnet.stellar.org/accounts/GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP/payments{?cursor,limit,order}",
"templated": true
},
"effects": {
"href": "https://horizon-testnet.stellar.org/accounts/GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP/effects{?cursor,limit,order}",
"templated": true
},
"offers": {
"href": "https://horizon-testnet.stellar.org/accounts/GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP/offers{?cursor,limit,order}",
"templated": true
},
"trades": {
"href": "https://horizon-testnet.stellar.org/accounts/GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP/trades{?cursor,limit,order}",
"templated": true
},
"data": {
"href": "https://horizon-testnet.stellar.org/accounts/GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP/data/{key}",
"templated": true
}
},
"id": "GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
"account_id": "GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
"sequence": "47236050321450",
"subentry_count": 0,
"last_modified_ledger": 116787,
"thresholds": {
"low_threshold": 0,
"med_threshold": 0,
"high_threshold": 0
},
"flags": {
"auth_required": false,
"auth_revocable": false,
"auth_immutable": false
},
"balances": [
{
"balance": "100.8182300",
"buying_liabilities": "0.0000000",
"selling_liabilities": "0.0000000",
"asset_type": "native"
}
],
"signers": [
{
"weight": 1,
"key": "GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP",
"type": "ed25519_public_key"
}
],
"data": {},
"paging_token": "GAI3SO3S4E67HAUZPZ2D3VBFXY4AT6N7WQI7K5WFGRXWENTZJG2B6CYP"
}
]
}
}
`

var accountResponse = `{
"_links": {
"self": {
Expand Down
6 changes: 6 additions & 0 deletions clients/horizonclient/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ type MockClient struct {
mock.Mock
}

// Accounts is a mocking method
func (m *MockClient) Accounts(request AccountsRequest) (hProtocol.AccountsPage, error) {
a := m.Called(request)
return a.Get(0).(hProtocol.AccountsPage), a.Error(1)
}

// AccountDetail is a mocking method
func (m *MockClient) AccountDetail(request AccountRequest) (hProtocol.Account, error) {
a := m.Called(request)
Expand Down
8 changes: 8 additions & 0 deletions protocols/horizon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,14 @@ type AccountData struct {
Value string `json:"value"`
}

// AccountsPage returns a list of account records
type AccountsPage struct {
Links hal.Links `json:"_links"`
Embedded struct {
Records []Account `json:"records"`
} `json:"_embedded"`
}

// TradeAggregationsPage returns a list of aggregated trade records, aggregated by resolution
type TradeAggregationsPage struct {
Links hal.Links `json:"_links"`
Expand Down

0 comments on commit 50cae75

Please sign in to comment.