diff --git a/clients/horizonclient/client.go b/clients/horizonclient/client.go index 37b4c62ef3..464919fbfc 100644 --- a/clients/horizonclient/client.go +++ b/clients/horizonclient/client.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "net/url" + "strconv" "strings" "time" @@ -345,6 +346,23 @@ func (c *Client) Offers(request OfferRequest) (offers hProtocol.OffersPage, err return } +// OfferDetails returns information for a single offer. +// See https://www.stellar.org/developers/horizon/reference/endpoints/offer-details.html +func (c *Client) OfferDetails(offerID string) (offer hProtocol.Offer, err error) { + if len(offerID) == 0 { + err = errors.New("no offer ID provided") + return + } + + if _, err = strconv.ParseInt(offerID, 10, 64); err != nil { + err = errors.New("invalid offer ID provided") + return + } + + err = c.sendRequest(OfferRequest{OfferID: offerID}, &offer) + return +} + // Operations returns stellar operations (https://www.stellar.org/developers/horizon/reference/resources/operation.html) // It can be used to return operations for an account, a ledger, a transaction and all operations on the network. func (c *Client) Operations(request OperationRequest) (ops operations.OperationsPage, err error) { diff --git a/clients/horizonclient/examples_test.go b/clients/horizonclient/examples_test.go index 50391cb683..0682bfa225 100644 --- a/clients/horizonclient/examples_test.go +++ b/clients/horizonclient/examples_test.go @@ -263,7 +263,6 @@ func ExampleClient_NextOffersPage() { fmt.Println(nextPage) } } - func ExampleClient_NextOperationsPage() { client := horizonclient.DefaultPublicNetClient // all operations @@ -410,6 +409,17 @@ func ExampleClient_NextTransactionsPage() { } } +func ExampleClient_OfferDetails() { + client := horizonclient.DefaultPublicNetClient + offer, err := client.OfferDetails("2") + if err != nil { + fmt.Println(err) + return + } + + fmt.Print(offer) +} + func ExampleClient_Offers() { client := horizonclient.DefaultPublicNetClient offerRequest := horizonclient.OfferRequest{ diff --git a/clients/horizonclient/main.go b/clients/horizonclient/main.go index 9226ef76f6..3d0158cd69 100644 --- a/clients/horizonclient/main.go +++ b/clients/horizonclient/main.go @@ -151,6 +151,7 @@ type ClientInterface interface { Metrics() (hProtocol.Metrics, error) FeeStats() (hProtocol.FeeStats, error) Offers(request OfferRequest) (hProtocol.OffersPage, error) + OfferDetails(offerID string) (offer hProtocol.Offer, err error) Operations(request OperationRequest) (operations.OperationsPage, error) OperationDetail(id string) (operations.Operation, error) SubmitTransactionXDR(transactionXdr string) (hProtocol.TransactionSuccess, error) @@ -275,9 +276,9 @@ type feeStatsRequest struct { } // OfferRequest struct contains data for getting offers made by an account from a horizon server. -// "ForAccount" is required. // The query parameters (Order, Cursor and Limit) are optional. All or none can be set. type OfferRequest struct { + OfferID string ForAccount string Selling string Seller string diff --git a/clients/horizonclient/main_test.go b/clients/horizonclient/main_test.go index c51c2311a9..fea7258ef6 100644 --- a/clients/horizonclient/main_test.go +++ b/clients/horizonclient/main_test.go @@ -566,6 +566,40 @@ func TestOfferRequest(t *testing.T) { assert.Len(t, offers.Embedded.Records, 1) } } +func TestOfferDetailsRequest(t *testing.T) { + hmock := httptest.NewClient() + client := &Client{ + HorizonURL: "https://localhost/", + HTTP: hmock, + } + + // account offers + hmock.On( + "GET", + "https://localhost/offers/5635", + ).ReturnString(200, offerResponse) + + record, err := client.OfferDetails("5635") + if assert.NoError(t, err) { + assert.IsType(t, record, hProtocol.Offer{}) + assert.Equal(t, record.ID, int64(5635)) + assert.Equal(t, record.Seller, "GD6UOZ3FGFI5L2X6F52YPJ6ICSW375BNBZIQC4PCLSEOO6SMX7CUS5MB") + assert.Equal(t, record.PT, "5635") + assert.Equal(t, record.Selling.Type, "native") + assert.Equal(t, record.Buying.Code, "AstroDollar") + assert.Equal(t, record.Buying.Issuer, "GDA2EHKPDEWZTAL6B66FO77HMOZL3RHZTIJO7KJJK5RQYSDUXEYMPJYY") + assert.Equal(t, record.Amount, "100.0000000") + assert.Equal(t, record.LastModifiedLedger, int32(356183)) + } + + _, err = client.OfferDetails("S6ES") + assert.Error(t, err) + assert.EqualError(t, err, "invalid offer ID provided") + + _, err = client.OfferDetails("") + assert.Error(t, err) + assert.EqualError(t, err, "no offer ID provided") +} func TestOperationsRequest(t *testing.T) { hmock := httptest.NewClient() @@ -1474,6 +1508,38 @@ var offersResponse = `{ } }` +var offerResponse = ` +{ + "_links": { + "self": { + "href": "https://horizon-testnet.stellar.org/offers/5635" + }, + "offer_maker": { + "href": "https://horizon-testnet.stellar.org/accounts/GD6UOZ3FGFI5L2X6F52YPJ6ICSW375BNBZIQC4PCLSEOO6SMX7CUS5MB" + } + }, + "id": "5635", + "paging_token": "5635", + "seller": "GD6UOZ3FGFI5L2X6F52YPJ6ICSW375BNBZIQC4PCLSEOO6SMX7CUS5MB", + "selling": { + "asset_type": "native" + }, + "buying": { + "asset_type": "credit_alphanum12", + "asset_code": "AstroDollar", + "asset_issuer": "GDA2EHKPDEWZTAL6B66FO77HMOZL3RHZTIJO7KJJK5RQYSDUXEYMPJYY" + }, + "amount": "100.0000000", + "price_r": { + "n": 10, + "d": 1 + }, + "price": "10.0000000", + "last_modified_ledger": 356183, + "last_modified_time": "2020-02-20T20:44:55Z" +} +` + var multipleOpsResponse = `{ "_links": { "self": { diff --git a/clients/horizonclient/mocks.go b/clients/horizonclient/mocks.go index 963ad2cbd7..c30fd25080 100644 --- a/clients/horizonclient/mocks.go +++ b/clients/horizonclient/mocks.go @@ -75,6 +75,12 @@ func (m *MockClient) Offers(request OfferRequest) (hProtocol.OffersPage, error) return a.Get(0).(hProtocol.OffersPage), a.Error(1) } +// OfferDetail is a mocking method +func (m *MockClient) OfferDetails(offerID string) (hProtocol.Offer, error) { + a := m.Called(offerID) + return a.Get(0).(hProtocol.Offer), a.Error(1) +} + // Operations is a mocking method func (m *MockClient) Operations(request OperationRequest) (operations.OperationsPage, error) { a := m.Called(request) diff --git a/clients/horizonclient/offer_request.go b/clients/horizonclient/offer_request.go index 635f49eb07..28cdb4dbe4 100644 --- a/clients/horizonclient/offer_request.go +++ b/clients/horizonclient/offer_request.go @@ -12,29 +12,33 @@ import ( // BuildURL creates the endpoint to be queried based on the data in the OfferRequest struct. func (or OfferRequest) BuildURL() (endpoint string, err error) { - // backwards compatibility support - if len(or.ForAccount) > 0 { - endpoint = fmt.Sprintf("accounts/%s/offers", or.ForAccount) - queryParams := addQueryParams(cursor(or.Cursor), limit(or.Limit), or.Order) - if queryParams != "" { - endpoint = fmt.Sprintf("%s?%s", endpoint, queryParams) - } + if len(or.OfferID) > 0 { + endpoint = fmt.Sprintf("offers/%s", or.OfferID) } else { - query := url.Values{} - if len(or.Seller) > 0 { - query.Add("seller", or.Seller) - } - if len(or.Selling) > 0 { - query.Add("selling", or.Selling) - } - if len(or.Buying) > 0 { - query.Add("buying", or.Buying) - } + // backwards compatibility support + if len(or.ForAccount) > 0 { + endpoint = fmt.Sprintf("accounts/%s/offers", or.ForAccount) + queryParams := addQueryParams(cursor(or.Cursor), limit(or.Limit), or.Order) + if queryParams != "" { + endpoint = fmt.Sprintf("%s?%s", endpoint, queryParams) + } + } else { + query := url.Values{} + if len(or.Seller) > 0 { + query.Add("seller", or.Seller) + } + if len(or.Selling) > 0 { + query.Add("selling", or.Selling) + } + if len(or.Buying) > 0 { + query.Add("buying", or.Buying) + } - endpoint = fmt.Sprintf("offers?%s", query.Encode()) - pageParams := addQueryParams(cursor(or.Cursor), limit(or.Limit), or.Order) - if pageParams != "" { - endpoint = fmt.Sprintf("%s&%s", endpoint, pageParams) + endpoint = fmt.Sprintf("offers?%s", query.Encode()) + pageParams := addQueryParams(cursor(or.Cursor), limit(or.Limit), or.Order) + if pageParams != "" { + endpoint = fmt.Sprintf("%s&%s", endpoint, pageParams) + } } } diff --git a/clients/horizonclient/offer_request_test.go b/clients/horizonclient/offer_request_test.go index cc01f05aef..d1a2975d21 100644 --- a/clients/horizonclient/offer_request_test.go +++ b/clients/horizonclient/offer_request_test.go @@ -25,6 +25,12 @@ func TestOfferRequestBuildUrl(t *testing.T) { // It should return valid offers endpoint and no errors require.NoError(t, err) assert.Equal(t, "accounts/GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU/offers?cursor=now&order=desc", endpoint) + + er = OfferRequest{OfferID: "12345"} + endpoint, err = er.BuildURL() + + require.NoError(t, err) + assert.Equal(t, "offers/12345", endpoint) } func TestNextOffersPage(t *testing.T) {