diff --git a/services/horizon/internal/actions_path_test.go b/services/horizon/internal/actions_path_test.go index ef915823d7..537a3211b4 100644 --- a/services/horizon/internal/actions_path_test.go +++ b/services/horizon/internal/actions_path_test.go @@ -50,76 +50,6 @@ func inMemoryPathFindingClient( return test.NewRequestHelper(router) } -func dbPathFindingClient( - tt *test.T, - maxAssetsParamLength int, -) test.RequestHelper { - router := chi.NewRouter() - findPaths := FindPathsHandler{ - pathFinder: &simplepath.Finder{ - Q: &core.Q{tt.CoreSession()}, - }, - maxAssetsParamLength: maxAssetsParamLength, - setLastLedgerHeader: false, - coreQ: &core.Q{tt.CoreSession()}, - } - findFixedPaths := FindFixedPathsHandler{ - pathFinder: &simplepath.Finder{ - Q: &core.Q{tt.CoreSession()}, - }, - maxAssetsParamLength: maxAssetsParamLength, - setLastLedgerHeader: false, - coreQ: &core.Q{tt.CoreSession()}, - } - - router.Group(func(r chi.Router) { - router.Method("GET", "/paths", findPaths) - router.Method("GET", "/paths/strict-receive", findPaths) - router.Method("GET", "/paths/strict-send", findFixedPaths) - }) - return test.NewRequestHelper(router) -} - -func TestPathActions_Index(t *testing.T) { - tt := test.Start(t).Scenario("paths") - assertions := &Assertions{tt.Assert} - defer tt.Finish() - rh := dbPathFindingClient( - tt, - 3, - ) - - // no query args - w := rh.Get("/paths") - assertions.Equal(400, w.Code) - - // happy path - var q = make(url.Values) - - q.Add( - "destination_account", - "GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V", - ) - q.Add( - "source_account", - "GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP", - ) - q.Add( - "destination_asset_issuer", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", - ) - q.Add("destination_asset_type", "credit_alphanum4") - q.Add("destination_asset_code", "EUR") - q.Add("destination_amount", "10") - - for _, uri := range []string{"/paths", "/paths/strict-receive"} { - w = rh.Get(uri + "?" + q.Encode()) - assertions.Equal(200, w.Code) - assertions.PageOf(3, w.Body) - assertions.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) - } -} - func TestPathActionsStillIngesting(t *testing.T) { tt := test.Start(t).Scenario("paths") defer tt.Finish() @@ -199,10 +129,6 @@ func TestPathActionsInMemoryFinder(t *testing.T) { orderBookGraph, len(sourceAssets), ) - dbPathsClient := dbPathFindingClient( - tt, - len(sourceAssets), - ) loadOffers(tt, orderBookGraph, "GA2NC4ZOXMXLVQAQQ5IQKJX47M3PKBQV2N5UV5Z4OXLQJ3CKMBA2O2YL", 1) loadOffers(tt, orderBookGraph, "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", 2) @@ -238,14 +164,7 @@ func TestPathActionsInMemoryFinder(t *testing.T) { tt.UnmarshalPage(w.Body, &inMemorySourceAccountResponse) tt.Assert.Equal("2", w.Header().Get(actions.LastLedgerHeaderName)) - w = dbPathsClient.Get(uri + "?" + withSourceAccount.Encode()) - tt.Assert.Equal(http.StatusOK, w.Code) - dbSourceAccountResponse := []horizon.Path{} - tt.UnmarshalPage(w.Body, &dbSourceAccountResponse) - tt.Assert.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) - tt.Assert.True(len(inMemorySourceAccountResponse) > 0) - tt.Assert.Equal(inMemorySourceAccountResponse, dbSourceAccountResponse) w = inMemoryPathsClient.Get(uri + "?" + withSourceAssets.Encode()) tt.Assert.Equal(http.StatusOK, w.Code) @@ -253,13 +172,6 @@ func TestPathActionsInMemoryFinder(t *testing.T) { tt.UnmarshalPage(w.Body, &inMemorySourceAssetsResponse) tt.Assert.Equal("2", w.Header().Get(actions.LastLedgerHeaderName)) - w = dbPathsClient.Get(uri + "?" + withSourceAccount.Encode()) - tt.Assert.Equal(http.StatusOK, w.Code) - dbSourceAssetsResponse := []horizon.Path{} - tt.UnmarshalPage(w.Body, &dbSourceAssetsResponse) - tt.Assert.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) - - tt.Assert.Equal(inMemorySourceAssetsResponse, dbSourceAssetsResponse) tt.Assert.Equal(inMemorySourceAssetsResponse, inMemorySourceAccountResponse) } } @@ -274,10 +186,6 @@ func TestPathActionsEmptySourceAcount(t *testing.T) { orderBookGraph, 3, ) - dbPathsClient := dbPathFindingClient( - tt, - 3, - ) loadOffers(tt, orderBookGraph, "GA2NC4ZOXMXLVQAQQ5IQKJX47M3PKBQV2N5UV5Z4OXLQJ3CKMBA2O2YL", 1) loadOffers(tt, orderBookGraph, "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", 2) @@ -308,13 +216,6 @@ func TestPathActionsEmptySourceAcount(t *testing.T) { tt.UnmarshalPage(w.Body, &inMemoryResponse) assertions.Empty(inMemoryResponse) tt.Assert.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) - - w = dbPathsClient.Get(uri + "?" + q.Encode()) - assertions.Equal(http.StatusOK, w.Code) - dbResponse := []horizon.Path{} - tt.UnmarshalPage(w.Body, &dbResponse) - assertions.Empty(dbResponse) - tt.Assert.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) } } diff --git a/services/horizon/internal/simplepath/doc.go b/services/horizon/internal/simplepath/doc.go index 1f77858729..1a3e71532b 100644 --- a/services/horizon/internal/simplepath/doc.go +++ b/services/horizon/internal/simplepath/doc.go @@ -1,5 +1,5 @@ // Package simplepath provides an implementation of paths. Finder that performs -// a breadth first search for paths against a stellar-core's database. +// a breadth first search for paths against an orderbook. // // The core algorithm works as follows: // 1. `search` object contains a queue of currently extended paths. Queue is diff --git a/services/horizon/internal/simplepath/finder.go b/services/horizon/internal/simplepath/finder.go deleted file mode 100644 index dbb568821e..0000000000 --- a/services/horizon/internal/simplepath/finder.go +++ /dev/null @@ -1,70 +0,0 @@ -package simplepath - -import ( - "github.com/go-errors/errors" - "github.com/stellar/go/services/horizon/internal/db2/core" - "github.com/stellar/go/services/horizon/internal/paths" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" -) - -// Finder implements the paths.Finder interface and searchs for -// payment paths using a simple breadth first search of the offers table of a stellar-core. -// -// This implementation is not meant to be fast or to provide the lowest costs paths, but -// rather is meant to be a simple implementation that gives usable paths. -type Finder struct { - Q *core.Q -} - -// ensure the struct is paths.Finder compliant -var _ paths.Finder = &Finder{} - -// Find performs a path find with the provided query. -func (f *Finder) Find(q paths.Query, maxLength uint) (result []paths.Path, lastLedger uint32, err error) { - log.WithField("source_assets", q.SourceAssets). - WithField("destination_asset", q.DestinationAsset). - WithField("destination_amount", q.DestinationAmount). - Info("Starting pathfind") - - if len(q.SourceAssets) == 0 { - err = errors.New("No source assets") - return - } - - if maxLength == 0 { - maxLength = MaxPathLength - } - - if maxLength < 2 || maxLength > MaxPathLength { - err = errors.New("invalid value of maxLength") - return - } - - s := &search{ - Query: q, - Q: &core.Q{f.Q.Clone()}, - MaxLength: maxLength, - } - - s.Init() - s.Run() - - result, err = s.Results, s.Err - - log.WithField("found", len(s.Results)). - WithField("err", s.Err). - Info("Finished pathfind") - return -} - -// FindFixedPaths will return an error because this implementation -// does not support this operation -func (f *Finder) FindFixedPaths( - sourceAsset xdr.Asset, - amountToSpend xdr.Int64, - destinationAssets []xdr.Asset, - maxLength uint, -) ([]paths.Path, uint32, error) { - return nil, 0, errors.New("Not implemented") -} diff --git a/services/horizon/internal/simplepath/finder_test.go b/services/horizon/internal/simplepath/finder_test.go deleted file mode 100644 index 84a9fb6427..0000000000 --- a/services/horizon/internal/simplepath/finder_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package simplepath - -import ( - "testing" - - "github.com/stellar/go/services/horizon/internal/db2/core" - "github.com/stellar/go/services/horizon/internal/paths" - "github.com/stellar/go/services/horizon/internal/test" - "github.com/stellar/go/xdr" -) - -func TestFinder(t *testing.T) { - tt := test.Start(t).Scenario("paths") - defer tt.Finish() - - finder := &Finder{ - Q: &core.Q{Session: tt.CoreSession()}, - } - - native := makeAsset(xdr.AssetTypeAssetTypeNative, "", "") - usd := makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "USD", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") - eur := makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "EUR", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") - inter1 := makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "1", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") - inter21 := makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "21", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") - inter22 := makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "22", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") - - query := paths.Query{ - DestinationAsset: eur, - DestinationAmount: xdr.Int64(200000000), // 20.0000000 - SourceAssets: []xdr.Asset{usd}, - } - - p, _, err := finder.Find(query, MaxPathLength) - if tt.Assert.NoError(err) { - tt.Assert.Len(p, 3) - - // Consuming offers: - // - selling 10 USD for EUR, price = 0.5 - // - selling 10 USD for EUR, price = 0.5 - tt.Assert.Equal(p[0].Source.String(), usd.String()) - tt.Assert.Equal(p[0].Destination.String(), eur.String()) - tt.Assert.Equal(p[0].SourceAmount, xdr.Int64(100000000)) // 10.0000000 - tt.Assert.Equal(p[0].DestinationAmount, xdr.Int64(200000000)) - tt.Assert.Len(p[0].Path, 0) - - // Consuming offers: - // - selling 20 USD for `1`, price = 1 - // - selling 20 `1` for EUR, price = 1 - tt.Assert.Equal(p[1].Source.String(), usd.String()) - tt.Assert.Equal(p[1].Destination.String(), eur.String()) - tt.Assert.Equal(p[1].SourceAmount, xdr.Int64(200000000)) - tt.Assert.Equal(p[1].DestinationAmount, xdr.Int64(200000000)) - if tt.Assert.Len(p[1].Path, 1) { - tt.Assert.Equal(p[1].Path[0].String(), inter1.String()) - } - - // Consuming offers: - // - selling 20 USD for `21`, price = 1 - // - selling 20 `21` for `22`, price = 1 - // - selling 20 `22` for EUR, price = 1 - tt.Assert.Equal(p[2].Source.String(), usd.String()) - tt.Assert.Equal(p[2].Destination.String(), eur.String()) - tt.Assert.Equal(p[2].SourceAmount, xdr.Int64(200000000)) - tt.Assert.Equal(p[2].DestinationAmount, xdr.Int64(200000000)) - if tt.Assert.Len(p[2].Path, 2) { - tt.Assert.Equal(p[2].Path[0].String(), inter21.String()) - tt.Assert.Equal(p[2].Path[1].String(), inter22.String()) - } - } - - query.DestinationAmount = xdr.Int64(200000001) - p, _, err = finder.Find(query, MaxPathLength) - if tt.Assert.NoError(err) { - tt.Assert.Len(p, 2) - - tt.Assert.Equal(p[0].Source.String(), usd.String()) - tt.Assert.Equal(p[0].Destination.String(), eur.String()) - tt.Assert.Equal(p[0].SourceAmount, xdr.Int64(100000001)) - tt.Assert.Equal(p[0].DestinationAmount, xdr.Int64(200000001)) - tt.Assert.Len(p[0].Path, 0) - - tt.Assert.Equal(p[1].Source.String(), usd.String()) - tt.Assert.Equal(p[1].Destination.String(), eur.String()) - tt.Assert.Equal(p[1].SourceAmount, xdr.Int64(200000001)) - tt.Assert.Equal(p[1].DestinationAmount, xdr.Int64(200000001)) - if tt.Assert.Len(p[1].Path, 2) { - tt.Assert.Equal(p[1].Path[0].String(), inter21.String()) - tt.Assert.Equal(p[1].Path[1].String(), inter22.String()) - } - } - - query.DestinationAmount = xdr.Int64(500000001) - p, _, err = finder.Find(query, MaxPathLength) - if tt.Assert.NoError(err) { - tt.Assert.Len(p, 0) - } - - // regression: paths that involve native currencies can be found - - query = paths.Query{ - DestinationAsset: native, - DestinationAmount: xdr.Int64(1), - SourceAssets: []xdr.Asset{usd, native}, - } - p, _, err = finder.Find(query, MaxPathLength) - if tt.Assert.NoError(err) { - tt.Assert.Len(p, 2) - } - - // In the past the order of trades was reversed, ex. the first trade was to sell - // destination amount, selling source, buying second asset on the path. The algorithm - // like this is wrong. - // - // Consider the following path: AAA -> BBB -> CCC, destination amount = 10 and the - // following offers: - // - // offer :trader, {for:["AAA", :gateway], sell:["BBB", :gateway]}, 1, 11 - // offer :trader, {for:["BBB", :gateway], sell:["CCC", :gateway]}, 10, 0.1 - // - // For such order books the old algorithm would not find a path as it is not - // possible to buy 1 BBB with 10 AAA and 1 BBB is needed to buy 10 CCC. - aaa := makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "AAA", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") - bbb := makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "BBB", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") - ccc := makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "CCC", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") - - query = paths.Query{ - DestinationAsset: ccc, - DestinationAmount: xdr.Int64(100000000), // 10.0 - SourceAssets: []xdr.Asset{aaa}, - } - p, _, err = finder.Find(query, MaxPathLength) - if tt.Assert.NoError(err) { - if tt.Assert.Len(p, 1) { - tt.Assert.Equal(p[0].Source.String(), aaa.String()) - tt.Assert.Equal(p[0].Destination.String(), ccc.String()) - tt.Assert.Equal(p[0].SourceAmount, xdr.Int64(110000000)) // 11.0 - tt.Assert.Equal(p[0].DestinationAmount, xdr.Int64(100000000)) // 10.0 - if tt.Assert.Len(p[0].Path, 1) { - tt.Assert.Equal(p[0].Path[0].String(), bbb.String()) - } - } - } -} diff --git a/services/horizon/internal/simplepath/helpers_test.go b/services/horizon/internal/simplepath/helpers_test.go deleted file mode 100644 index d44f6970a0..0000000000 --- a/services/horizon/internal/simplepath/helpers_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package simplepath - -import ( - "github.com/stellar/go/strkey" - "github.com/stellar/go/xdr" -) - -func makeAsset(typ xdr.AssetType, code string, issuer string) xdr.Asset { - - if typ == xdr.AssetTypeAssetTypeNative { - result, _ := xdr.NewAsset(typ, nil) - return result - } - - an := xdr.AssetAlphaNum4{} - copy(an.AssetCode[:], code[:]) - - raw := strkey.MustDecode(strkey.VersionByteAccountID, issuer) - var key xdr.Uint256 - copy(key[:], raw) - - an.Issuer, _ = xdr.NewAccountId(xdr.PublicKeyTypePublicKeyTypeEd25519, key) - - result, _ := xdr.NewAsset(typ, an) - return result -} diff --git a/services/horizon/internal/simplepath/order_book.go b/services/horizon/internal/simplepath/order_book.go deleted file mode 100644 index 7d064c4f2e..0000000000 --- a/services/horizon/internal/simplepath/order_book.go +++ /dev/null @@ -1,139 +0,0 @@ -package simplepath - -import ( - "errors" - "fmt" - "math" - - sq "github.com/Masterminds/squirrel" - "github.com/stellar/go/price" - "github.com/stellar/go/services/horizon/internal/db2/core" - "github.com/stellar/go/xdr" -) - -// ErrNotEnough represents an error that occurs when pricing a trade on an -// orderbook. This error occurs when the orderbook cannot fulfill the -// requested amount. -var ErrNotEnough = errors.New("not enough depth") - -// orderbook represents a one-way orderbook that is selling you a specific asset (ob.Selling) -type orderBook struct { - Selling xdr.Asset // the offers are selling this asset - Buying xdr.Asset // the offers are buying this asset - Q *core.Q -} - -// CostToConsumeLiquidity returns the buyingAmount (ob.Buying) needed to consume the sellingAmount (ob.Selling) -func (ob *orderBook) CostToConsumeLiquidity(sellingAmount xdr.Int64) (xdr.Int64, error) { - // load orderbook from core's db - sql, e := ob.query() - if e != nil { - return 0, e - } - rows, e := ob.Q.Query(sql) - if e != nil { - return 0, e - } - defer rows.Close() - - // remaining is the units of ob.Selling that we want to consume - remaining := int64(sellingAmount) - var buyingAmount int64 - for rows.Next() { - // load data from the row - var offerAmount, pricen, priced, offerid int64 - e = rows.Scan(&offerAmount, &pricen, &priced, &offerid) - if e != nil { - return 0, e - } - - buyingUnitsExtracted, sellingUnitsExtracted, e := price.ConvertToBuyingUnits(offerAmount, remaining, pricen, priced) - if e != nil { - return 0, e - } - // overflow check - if willAddOverflow(buyingAmount, buyingUnitsExtracted) { - return xdr.Int64(0), fmt.Errorf("adding these two values will cause an integer overflow: %d, %d", buyingAmount, buyingUnitsExtracted) - } - buyingAmount += buyingUnitsExtracted - remaining -= sellingUnitsExtracted - - // check if we got all the units we wanted - if remaining <= 0 { - return xdr.Int64(buyingAmount), nil - } - } - return 0, ErrNotEnough -} - -func willAddOverflow(a int64, b int64) bool { - return a > math.MaxInt64-b -} - -func (ob *orderBook) query() (sq.SelectBuilder, error) { - schemaVersion, err := ob.Q.SchemaVersion() - if err != nil { - return sq.SelectBuilder{}, err - } - - if schemaVersion < 9 { - return ob.querySchema8() - } else { - return ob.querySchema9() - } -} - -func (ob *orderBook) querySchema8() (sq.SelectBuilder, error) { - var ( - // selling/buying types - st, bt xdr.AssetType - // selling/buying codes - sc, bc string - // selling/buying issuers - si, bi string - ) - e := ob.Selling.Extract(&st, &sc, &si) - if e != nil { - return sq.SelectBuilder{}, e - } - e = ob.Buying.Extract(&bt, &bc, &bi) - if e != nil { - return sq.SelectBuilder{}, e - } - - sql := sq. - Select("amount", "pricen", "priced", "offerid"). - From("offers"). - Where(sq.Eq{ - "sellingassettype": st, - "COALESCE(sellingassetcode, '')": sc, - "COALESCE(sellingissuer, '')": si}). - Where(sq.Eq{ - "buyingassettype": bt, - "COALESCE(buyingassetcode, '')": bc, - "COALESCE(buyingissuer, '')": bi}). - OrderBy("price ASC") - return sql, nil -} - -func (ob *orderBook) querySchema9() (sq.SelectBuilder, error) { - var sellingXDRString, buyingXDRString string - - sellingXDRString, err := xdr.MarshalBase64(ob.Selling) - if err != nil { - return sq.SelectBuilder{}, err - } - - buyingXDRString, err = xdr.MarshalBase64(ob.Buying) - if err != nil { - return sq.SelectBuilder{}, err - } - - sql := sq. - Select("amount", "pricen", "priced", "offerid"). - From("offers"). - Where(sq.Eq{"sellingasset": sellingXDRString}). - Where(sq.Eq{"buyingasset": buyingXDRString}). - OrderBy("price ASC") - return sql, nil -} diff --git a/services/horizon/internal/simplepath/order_book_test.go b/services/horizon/internal/simplepath/order_book_test.go deleted file mode 100644 index 4775cce922..0000000000 --- a/services/horizon/internal/simplepath/order_book_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package simplepath - -import ( - "math" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/stellar/go/services/horizon/internal/db2/core" - "github.com/stellar/go/services/horizon/internal/test" - "github.com/stellar/go/xdr" -) - -func TestOrderBook(t *testing.T) { - tt := test.Start(t).Scenario("paths") - defer tt.Finish() - - ob := orderBook{ - Selling: makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "EUR", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN"), - Buying: makeAsset( - xdr.AssetTypeAssetTypeCreditAlphanum4, - "USD", - "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN"), - Q: &core.Q{Session: tt.CoreSession()}, - } - - testCases := []struct { - scenario string - eur int64 - wantCostUSD int64 - }{ - {"first unit", 2, 1}, // taking from the first offer, where the price is 0.25 (i.e. offer to sell 4 EUR for 1 USD) - {"first full offer", 100000000, 50000000}, // taking all from first offer (p=0.25) - {"first full offer + 1", 100000002, 50000001}, // taking all from first offer (p=0.25), and first full unit of second offer (p=0.5) - {"first two full offers", 200000000, 100000000}, // taking all from first two offers (p=0.25, p=0.5) - {"first two full offers + 1", 200000001, 100000001}, // taking all from first two offers (p=0.25, p=0.5), and first full unit of third offer (p=1.0) - {"first three full offers", 300000000, 200000000}, // taking all from first three offers (p=0.25, p=0.5, p = 1.0) - } - - for _, kase := range testCases { - t.Run(kase.scenario, func(t *testing.T) { - r, err := ob.CostToConsumeLiquidity(xdr.Int64(kase.eur)) - if tt.Assert.NoError(err) { - tt.Assert.Equal(xdr.Int64(kase.wantCostUSD), r) - } - }) - } - - // taking 1 more than there is available - t.Run("one more than available liquidity", func(t *testing.T) { - _, err := ob.CostToConsumeLiquidity(xdr.Int64(300000001)) - tt.Assert.Error(err) - }) -} - -func TestWillAddOverflow(t *testing.T) { - testCases := []struct { - a int64 - b int64 - wantWillOverflow bool - }{ - {1, 2, false}, - {0, 1, false}, - {math.MaxInt64, 0, false}, - {math.MaxInt64 - 1, 1, false}, - {math.MaxInt64, 1, true}, - {math.MaxInt64 - 1, 2, true}, - {math.MaxInt64 - 1, math.MaxInt64, true}, - {math.MaxInt64, math.MaxInt64, true}, - } - for _, kase := range testCases { - t.Run(t.Name(), func(t *testing.T) { - r := willAddOverflow(kase.a, kase.b) - assert.Equal(t, kase.wantWillOverflow, r) - }) - } -} diff --git a/services/horizon/internal/simplepath/path.go b/services/horizon/internal/simplepath/path.go deleted file mode 100644 index e66d7ff33d..0000000000 --- a/services/horizon/internal/simplepath/path.go +++ /dev/null @@ -1,147 +0,0 @@ -package simplepath - -import ( - "bytes" - "fmt" - - "github.com/stellar/go/services/horizon/internal/db2/core" - "github.com/stellar/go/xdr" -) - -// pathNode represents a path as a linked list pointing from source to destination -type pathNode struct { - Asset xdr.Asset - Tail *pathNode - Q *core.Q - CachedCost *xdr.Int64 - Depth uint -} - -func (p *pathNode) String() string { - if p == nil { - return "" - } - - var out bytes.Buffer - fmt.Fprintf(&out, "%v", p.Asset) - - cur := p.Tail - - for cur != nil { - fmt.Fprintf(&out, " -> %v", cur.Asset) - cur = cur.Tail - } - - return out.String() -} - -// Destination returns the destination of the pathNode -func (p *pathNode) Destination() xdr.Asset { - cur := p - for cur.Tail != nil { - cur = cur.Tail - } - return cur.Asset -} - -// IsOnPath returns true if a given asset is in the path. -func (p *pathNode) IsOnPath(asset xdr.Asset) bool { - cur := p - for cur.Tail != nil { - if asset.Equals(cur.Asset) { - return true - } - cur = cur.Tail - } - - return asset.Equals(cur.Asset) -} - -// Source returns the source asset in the pathNode -func (p *pathNode) Source() xdr.Asset { - // the destination for path is the head of the linked list - return p.Asset -} - -// Path returns the path of the list excluding the source and destination assets -func (p *pathNode) Path() []xdr.Asset { - path := p.Flatten() - - if len(path) < 2 { - return nil - } - - // return the flattened slice without the first and last elements - // which are the source and the destination assets - return path[1 : len(path)-1] -} - -// Cost computes the units of the source asset needed to send the amount in the destination asset -// This is an expensive operation so callers should reuse the result where appropriate -func (p *pathNode) Cost(amount xdr.Int64) (xdr.Int64, error) { - if p.Tail == nil { - return amount, nil - } - - if p.CachedCost != nil { - return *p.CachedCost, nil - } - - // The first element of the current path is the current source asset. - // The last element (with `Tail` = nil) of the current path is the destination - // asset. To make the calculations correct we start by selling destination - // asset to the second from the end asset and continue until we reach the current - // source asset. - cur := p - stack := make([]*pathNode, 0, p.Depth) - for cur.Tail != nil { - stack = append(stack, cur) - cur = cur.Tail - } - - var err error - result := amount - - for i := len(stack) - 1; i >= 0; i-- { - cur = stack[i] - - if cur.CachedCost != nil { - result = *cur.CachedCost - continue - } - - ob := cur.OrderBook() - result, err = ob.CostToConsumeLiquidity(result) - if err != nil { - return result, err - } - } - - // Cache the result - cur.CachedCost = &result - - return result, nil -} - -func (p *pathNode) OrderBook() *orderBook { - if p.Tail == nil { - return nil - } - - return &orderBook{ - Selling: p.Tail.Asset, // offer is selling this asset - Buying: p.Asset, // offer is buying this asset - Q: p.Q, - } -} - -// Flatten walks the list and returns a slice of assets -func (p *pathNode) Flatten() []xdr.Asset { - result := []xdr.Asset{} - cur := p - for cur != nil { - result = append(result, cur.Asset) - cur = cur.Tail - } - return result -} diff --git a/services/horizon/internal/simplepath/search.go b/services/horizon/internal/simplepath/search.go deleted file mode 100644 index d2ca835c87..0000000000 --- a/services/horizon/internal/simplepath/search.go +++ /dev/null @@ -1,218 +0,0 @@ -package simplepath - -import ( - "github.com/stellar/go/services/horizon/internal/db2/core" - "github.com/stellar/go/services/horizon/internal/paths" - "github.com/stellar/go/xdr" -) - -// MaxPathLength is a maximum path length as defined in XDR file (includes source and -// destination assets). -const MaxPathLength uint = 7 - -// search represents a single query against the simple finder. It provides -// a place to store the results of the query, mostly for the purposes of code -// clarity. -// -// The search struct is used as follows: -// -// 1. Create an instance, ensuring the Query and Finder fields are set -// 2. Call Init() to populate dependent fields in the struct with their initial values -// 3. Call Run() to perform the search. -// -type search struct { - Query paths.Query - Q *core.Q - MaxLength uint - - // Fields below are initialized by a call to Init() after - // setting the fields above - queue []computedNode - targets map[string]bool - - //This fields below are initialized after the search is run - Err error - Results []paths.Path -} - -// computedNode represents a pathNode with the computed cost -type computedNode struct { - path pathNode - cost xdr.Int64 -} - -func (c computedNode) asPath(destinationAmount xdr.Int64) paths.Path { - return paths.Path{ - Path: c.path.Path(), - Source: c.path.Source(), - SourceAmount: c.cost, - Destination: c.path.Destination(), - DestinationAmount: destinationAmount, - } -} - -const maxResults = 20 - -// Init initialized the search, setting fields on the struct used to -// hold state needed during the actual search. -func (s *search) Init() { - p0 := pathNode{ - Asset: s.Query.DestinationAsset, - Tail: nil, - Q: s.Q, - Depth: 1, - } - var c0 xdr.Int64 - // `Cost` on destination node does not use DB connection. - c0, s.Err = p0.Cost(s.Query.DestinationAmount) - if s.Err != nil { - return - } - - s.queue = []computedNode{ - computedNode{ - path: p0, - cost: c0, - }, - } - - // build a map of asset's string representation to check if a given node - // is one of the targets for our search. Unfortunately, xdr.Asset is not suitable - // for use as a map key, and so we use its string representation. - s.targets = map[string]bool{} - for _, a := range s.Query.SourceAssets { - s.targets[a.String()] = true - } - - s.Err = nil - s.Results = nil -} - -// Run triggers the search, which will populate the Results and Err -// field for the search after completion. -func (s *search) Run() { - if s.Err != nil { - return - } - - s.Err = s.Q.Begin() - if s.Err != nil { - return - } - - defer s.Q.Rollback() - - // We need REPEATABLE READ here to have a stable view of the offers - // table. Without it, it's possible that search started in ledger X - // and finished in ledger X+1 would give invalid results. - // - // https://www.postgresql.org/docs/9.1/static/transaction-iso.html - // > Note that only updating transactions might need to be retried; - // > read-only transactions will never have serialization conflicts. - _, s.Err = s.Q.ExecRaw("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY") - if s.Err != nil { - return - } - - for s.hasMore() { - s.runOnce() - } -} - -// pop removes the head from the search queue, returning it to the caller -func (s *search) pop() computedNode { - next := s.queue[0] - s.queue = s.queue[1:] - return next -} - -// returns false if the search should stop. -func (s *search) hasMore() bool { - if s.Err != nil { - return false - } - - if len(s.Results) >= maxResults { - return false - } - - return len(s.queue) > 0 -} - -// isTarget returns true if the asset id provided is one of the targets -// for this search (i.e. one of the requesting account's trusted assets) -func (s *search) isTarget(id string) bool { - _, found := s.targets[id] - return found -} - -// runOnce processes the head of the search queue, findings results -// and extending the search as necessary. -func (s *search) runOnce() { - cur := s.pop() - id := cur.path.Asset.String() - - if s.isTarget(id) { - s.Results = append(s.Results, cur.asPath(s.Query.DestinationAmount)) - } - - if cur.path.Depth == s.MaxLength { - return - } - - s.extendSearch(cur.path) -} - -func (s *search) extendSearch(p pathNode) { - // find connected assets - var connected []xdr.Asset - s.Err = s.Q.ConnectedAssets(&connected, p.Asset) - if s.Err != nil { - return - } - - for _, a := range connected { - // If asset already exists on the path, continue to the next one. - // We don't want the same asset on the path twice as buying and - // then selling the asset will be a bad deal in most cases - // (especially A -> B -> A trades). - if p.IsOnPath(a) { - continue - } - - // If the connected asset is not our target and the current length - // of the path is MaxLength-1 then it does not make sense to extend - // such path. - if p.Depth == s.MaxLength-1 && !s.isTarget(a.String()) { - continue - } - - newPath := pathNode{ - Asset: a, - Tail: &p, - Q: s.Q, - Depth: p.Depth + 1, - } - - var hasEnough bool - var cost xdr.Int64 - hasEnough, cost, s.Err = s.hasEnoughDepth(&newPath) - if s.Err != nil { - return - } - - if !hasEnough { - continue - } - - s.queue = append(s.queue, computedNode{newPath, cost}) - } -} - -func (s *search) hasEnoughDepth(path *pathNode) (bool, xdr.Int64, error) { - cost, err := path.Cost(s.Query.DestinationAmount) - if err == ErrNotEnough { - return false, 0, nil - } - return true, cost, err -}