Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

services/horizon/internal/actions: Add /order_book ingestion endpoint #1761

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions exp/orderbook/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,55 @@ func (graph *OrderBookGraph) batch() *orderBookBatchedUpdates {
}
}

// findOffers returns all offers for a given trading pair
// The offers will be sorted by price from cheapest to most expensive
// The returned offers will span at most `maxPriceLevels` price levels
func (graph *OrderBookGraph) findOffers(
selling, buying string, maxPriceLevels int,
) []xdr.OfferEntry {
results := []xdr.OfferEntry{}
edges, ok := graph.edgesForSellingAsset[selling]
if !ok {
return results
}
offers, ok := edges[buying]
if !ok {
return results
}

for _, offer := range offers {
if len(results) == 0 || results[len(results)-1].Price != offer.Price {
maxPriceLevels--
}
if maxPriceLevels < 0 {
return results
}

results = append(results, offer)
}
return results
}

// FindAsksAndBids returns all asks and bids for a given trading pair
// Asks consists of all offers which sell `selling` in exchange for `buying` sorted by
// price (in terms of `buying`) from cheapest to most expensive
// Bids consists of all offers which sell `buying` in exchange for `selling` sorted by
// price (in terms of `selling`) from cheapest to most expensive
// Both Asks and Bids will span at most `maxPriceLevels` price levels
func (graph *OrderBookGraph) FindAsksAndBids(
selling, buying xdr.Asset, maxPriceLevels int,
) ([]xdr.OfferEntry, []xdr.OfferEntry) {
buyingString := buying.String()
sellingString := selling.String()

graph.lock.RLock()
defer graph.lock.RUnlock()
asks := graph.findOffers(sellingString, buyingString, maxPriceLevels)
bids := graph.findOffers(buyingString, sellingString, maxPriceLevels)

return asks, bids
}

// add inserts a given offer into the order book graph
func (graph *OrderBookGraph) add(offer xdr.OfferEntry) error {
if _, contains := graph.tradingPairForOffer[offer.OfferId]; contains {
Expand Down
137 changes: 137 additions & 0 deletions exp/orderbook/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,143 @@ func TestRemoveOfferOrderBook(t *testing.T) {
}
}

func TestFindOffers(t *testing.T) {
graph := NewOrderBookGraph()

assertOfferListEquals(
t,
[]xdr.OfferEntry{},
graph.findOffers(nativeAsset.String(), eurAsset.String(), 0),
)

assertOfferListEquals(
t,
[]xdr.OfferEntry{},
graph.findOffers(nativeAsset.String(), eurAsset.String(), 5),
)

err := graph.
AddOffer(threeEurOffer).
AddOffer(eurOffer).
AddOffer(twoEurOffer).
Apply()
if err != nil {
t.Fatalf("unexpected error %v", err)
}

assertOfferListEquals(
t,
[]xdr.OfferEntry{},
graph.findOffers(nativeAsset.String(), eurAsset.String(), 0),
)
assertOfferListEquals(
t,
[]xdr.OfferEntry{eurOffer, twoEurOffer},
graph.findOffers(nativeAsset.String(), eurAsset.String(), 2),
)

extraTwoEurOffers := []xdr.OfferEntry{}
for i := 0; i < 4; i++ {
otherTwoEurOffer := twoEurOffer
otherTwoEurOffer.OfferId += xdr.Int64(i + 17)
graph.AddOffer(otherTwoEurOffer)
extraTwoEurOffers = append(extraTwoEurOffers, otherTwoEurOffer)
}
if err := graph.Apply(); err != nil {
t.Fatalf("unexpected error %v", err)
}

assertOfferListEquals(
t,
append([]xdr.OfferEntry{eurOffer, twoEurOffer}, extraTwoEurOffers...),
graph.findOffers(nativeAsset.String(), eurAsset.String(), 2),
)
assertOfferListEquals(
t,
append(append([]xdr.OfferEntry{eurOffer, twoEurOffer}, extraTwoEurOffers...), threeEurOffer),
graph.findOffers(nativeAsset.String(), eurAsset.String(), 3),
)
}

func TestFindAsksAndBids(t *testing.T) {
graph := NewOrderBookGraph()

asks, bids := graph.FindAsksAndBids(nativeAsset, eurAsset, 0)
assertOfferListEquals(
t,
[]xdr.OfferEntry{},
asks,
)
assertOfferListEquals(
t,
[]xdr.OfferEntry{},
bids,
)

asks, bids = graph.FindAsksAndBids(nativeAsset, eurAsset, 5)
assertOfferListEquals(
t,
[]xdr.OfferEntry{},
asks,
)
assertOfferListEquals(
t,
[]xdr.OfferEntry{},
bids,
)

err := graph.
AddOffer(threeEurOffer).
AddOffer(eurOffer).
AddOffer(twoEurOffer).
Apply()
if err != nil {
t.Fatalf("unexpected error %v", err)
}

asks, bids = graph.FindAsksAndBids(nativeAsset, eurAsset, 0)
assertOfferListEquals(
t,
[]xdr.OfferEntry{},
asks,
)
assertOfferListEquals(
t,
[]xdr.OfferEntry{},
bids,
)

extraTwoEurOffers := []xdr.OfferEntry{}
for i := 0; i < 4; i++ {
otherTwoEurOffer := twoEurOffer
otherTwoEurOffer.OfferId += xdr.Int64(i + 17)
graph.AddOffer(otherTwoEurOffer)
extraTwoEurOffers = append(extraTwoEurOffers, otherTwoEurOffer)
}
if err := graph.Apply(); err != nil {
t.Fatalf("unexpected error %v", err)
}

sellEurOffer := twoEurOffer
sellEurOffer.Buying, sellEurOffer.Selling = sellEurOffer.Selling, sellEurOffer.Buying
sellEurOffer.OfferId = 35
if err := graph.AddOffer(sellEurOffer).Apply(); err != nil {
t.Fatalf("unexpected error %v", err)
}

asks, bids = graph.FindAsksAndBids(nativeAsset, eurAsset, 3)
assertOfferListEquals(
t,
append(append([]xdr.OfferEntry{eurOffer, twoEurOffer}, extraTwoEurOffers...), threeEurOffer),
asks,
)
assertOfferListEquals(
t,
[]xdr.OfferEntry{sellEurOffer},
bids,
)
}

func TestConsumeOffersForSellingAsset(t *testing.T) {
kp, err := keypair.Random()
if err != nil {
Expand Down
23 changes: 23 additions & 0 deletions services/horizon/internal/actions/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,3 +498,26 @@ func testURLParams() map[string]string {
"long_12_asset_issuer": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H",
}
}

func makeRequest(
t *testing.T,
queryParams map[string]string,
routeParams map[string]string,
) *http.Request {
request, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
query := url.Values{}
for key, value := range queryParams {
query.Set(key, value)
}
request.URL.RawQuery = query.Encode()

chiRouteContext := chi.NewRouteContext()
for key, value := range routeParams {
chiRouteContext.URLParams.Add(key, value)
}
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiRouteContext)
return request.WithContext(ctx)
}
75 changes: 20 additions & 55 deletions services/horizon/internal/actions/offer_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package actions

import (
"context"
"net/http"
"net/url"
"testing"
"time"

"github.com/go-chi/chi"
"github.com/stellar/go/protocols/horizon"
"github.com/stellar/go/services/horizon/internal/db2/core"
"github.com/stellar/go/services/horizon/internal/db2/history"
Expand Down Expand Up @@ -63,50 +59,6 @@ var (
}
)

func makeOffersRequest(t *testing.T, queryParams map[string]string) *http.Request {
request, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
query := url.Values{}
for key, value := range queryParams {
query.Set(key, value)
}
request.URL.RawQuery = query.Encode()

ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chi.NewRouteContext())
return request.WithContext(ctx)
}

func makeAccountOffersRequest(
t *testing.T,
accountID string,
queryParams map[string]string,
) *http.Request {
request, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
query := url.Values{}
for key, value := range queryParams {
query.Set(key, value)
}
request.URL.RawQuery = query.Encode()

chiRouteContext := chi.NewRouteContext()
chiRouteContext.URLParams.Add("account_id", accountID)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiRouteContext)
return request.WithContext(ctx)
}

func pageableToOffers(t *testing.T, page []hal.Pageable) []horizon.Offer {
var offers []horizon.Offer
for _, entry := range page {
offers = append(offers, entry.(horizon.Offer))
}
return offers
}

func TestGetOffersHandler(t *testing.T) {
tt := test.Start(t)
defer tt.Finish()
Expand All @@ -133,7 +85,7 @@ func TestGetOffersHandler(t *testing.T) {
tt.Assert.NoError(q.UpsertOffer(usdOffer, 3))

t.Run("No filter", func(t *testing.T) {
records, err := handler.GetResourcePage(makeOffersRequest(t, map[string]string{}))
records, err := handler.GetResourcePage(makeRequest(t, map[string]string{}, map[string]string{}))
tt.Assert.NoError(err)
tt.Assert.Len(records, 3)

Expand All @@ -149,11 +101,12 @@ func TestGetOffersHandler(t *testing.T) {
})

t.Run("Filter by seller", func(t *testing.T) {
records, err := handler.GetResourcePage(makeOffersRequest(
records, err := handler.GetResourcePage(makeRequest(
t,
map[string]string{
"seller": issuer.Address(),
},
map[string]string{},
))
tt.Assert.NoError(err)
tt.Assert.Len(records, 2)
Expand All @@ -168,11 +121,12 @@ func TestGetOffersHandler(t *testing.T) {
asset := horizon.Asset{}
nativeAsset.Extract(&asset.Type, &asset.Code, &asset.Issuer)

records, err := handler.GetResourcePage(makeOffersRequest(
records, err := handler.GetResourcePage(makeRequest(
t,
map[string]string{
"selling_asset_type": asset.Type,
},
map[string]string{},
))
tt.Assert.NoError(err)
tt.Assert.Len(records, 2)
Expand All @@ -185,13 +139,14 @@ func TestGetOffersHandler(t *testing.T) {
asset = horizon.Asset{}
eurAsset.Extract(&asset.Type, &asset.Code, &asset.Issuer)

records, err = handler.GetResourcePage(makeOffersRequest(
records, err = handler.GetResourcePage(makeRequest(
t,
map[string]string{
"selling_asset_type": asset.Type,
"selling_asset_code": asset.Code,
"selling_asset_issuer": asset.Issuer,
},
map[string]string{},
))
tt.Assert.NoError(err)
tt.Assert.Len(records, 1)
Expand All @@ -204,13 +159,14 @@ func TestGetOffersHandler(t *testing.T) {
asset := horizon.Asset{}
eurAsset.Extract(&asset.Type, &asset.Code, &asset.Issuer)

records, err := handler.GetResourcePage(makeOffersRequest(
records, err := handler.GetResourcePage(makeRequest(
t,
map[string]string{
"buying_asset_type": asset.Type,
"buying_asset_code": asset.Code,
"buying_asset_issuer": asset.Issuer,
},
map[string]string{},
))
tt.Assert.NoError(err)
tt.Assert.Len(records, 2)
Expand All @@ -223,13 +179,14 @@ func TestGetOffersHandler(t *testing.T) {
asset = horizon.Asset{}
usdAsset.Extract(&asset.Type, &asset.Code, &asset.Issuer)

records, err = handler.GetResourcePage(makeOffersRequest(
records, err = handler.GetResourcePage(makeRequest(
t,
map[string]string{
"buying_asset_type": asset.Type,
"buying_asset_code": asset.Code,
"buying_asset_issuer": asset.Issuer,
},
map[string]string{},
))
tt.Assert.NoError(err)
tt.Assert.Len(records, 1)
Expand All @@ -256,7 +213,7 @@ func TestGetAccountOffersHandler(t *testing.T) {
tt.Assert.NoError(q.UpsertOffer(usdOffer, 3))

records, err := handler.GetResourcePage(
makeAccountOffersRequest(t, issuer.Address(), map[string]string{}),
makeRequest(t, map[string]string{}, map[string]string{"account_id": issuer.Address()}),
)
tt.Assert.NoError(err)
tt.Assert.Len(records, 2)
Expand All @@ -267,3 +224,11 @@ func TestGetAccountOffersHandler(t *testing.T) {
tt.Assert.Equal(issuer.Address(), offer.Seller)
}
}

func pageableToOffers(t *testing.T, page []hal.Pageable) []horizon.Offer {
var offers []horizon.Offer
for _, entry := range page {
offers = append(offers, entry.(horizon.Offer))
}
return offers
}
Loading