Skip to content

Commit

Permalink
clients/horizonclient: Add support for Accounts endpoint.
Browse files Browse the repository at this point in the history
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)
```
  • Loading branch information
abuiles committed Feb 5, 2020
1 parent 9c56e99 commit f9a0229
Show file tree
Hide file tree
Showing 7 changed files with 273 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.
// "Signer" or "Asset" fields should be set when retrieving AccountsData.
// 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
}
11 changes: 11 additions & 0 deletions clients/horizonclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,17 @@ 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)
if err != nil {
return
}
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.
// "Signer" or "Asset" fields should be set when retrieving AccountsData.
// At the moment, you can't use both filters are 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 @@ -738,6 +818,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 @@ -523,6 +523,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 f9a0229

Please sign in to comment.