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: Add an endpoint that allows querying for which liquidity pools an account is participating in #4043

Merged
merged 20 commits into from
Nov 4, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
6 changes: 4 additions & 2 deletions services/horizon/internal/actions/liquidity_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ func (handler GetLiquidityPoolByIDHandler) GetResource(w HeaderWriter, r *http.R
// LiquidityPoolsQuery query struct for liquidity_pools end-point
type LiquidityPoolsQuery struct {
Reserves string `schema:"reserves" valid:"optional"`
Account string `schema:"account" valid:"optional"`

reserves []xdr.Asset
}

// URITemplate returns a rfc6570 URI template the query struct
func (q LiquidityPoolsQuery) URITemplate() string {
return "/liquidity_pools?{?reserves}"
return "/liquidity_pools?{?reserves,?account}"
erika-sdf marked this conversation as resolved.
Show resolved Hide resolved
}

// Validate validates and parses the query
Expand Down Expand Up @@ -105,7 +106,7 @@ type GetLiquidityPoolsHandler struct {
LedgerState *ledger.State
}

// GetResourcePage returns a page of claimable balances.
// GetResourcePage returns a page of liquidity pools.
func (handler GetLiquidityPoolsHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) {
ctx := r.Context()
qp := LiquidityPoolsQuery{}
Expand All @@ -121,6 +122,7 @@ func (handler GetLiquidityPoolsHandler) GetResourcePage(w HeaderWriter, r *http.

query := history.LiquidityPoolsQuery{
PageQuery: pq,
AccountID: qp.Account,
Assets: qp.reserves,
}

Expand Down
155 changes: 68 additions & 87 deletions services/horizon/internal/actions/liquidity_pool_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package actions

import (
"fmt"
"net/http/httptest"
"testing"

"github.com/stellar/go/keypair"
protocol "github.com/stellar/go/protocols/horizon"
"github.com/stellar/go/services/horizon/internal/db2/history"
"github.com/stellar/go/services/horizon/internal/test"
"github.com/stellar/go/support/errors"
"github.com/stellar/go/support/render/problem"
"github.com/stellar/go/xdr"
"github.com/stretchr/testify/assert"
)

func TestGetLiquidityPoolByID(t *testing.T) {
Expand All @@ -18,25 +21,7 @@ func TestGetLiquidityPoolByID(t *testing.T) {
test.ResetHorizonDB(t, tt.HorizonDB)
q := &history.Q{tt.HorizonSession()}

lp := history.LiquidityPool{
PoolID: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct,
Fee: 30,
TrustlineCount: 100,
ShareCount: 2000000000,
AssetReserves: history.LiquidityPoolAssetReserves{
{
Asset: xdr.MustNewNativeAsset(),
Reserve: 100,
},
{
Asset: xdr.MustNewCreditAsset("USD", "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"),
Reserve: 200,
},
},
LastModifiedLedger: 100,
}

lp := history.MakeTestPool(xdr.MustNewNativeAsset(), 100, usdAsset, 200)
err := q.UpsertLiquidityPools(tt.Ctx, []history.LiquidityPool{lp})
tt.Assert.NoError(err)

Expand All @@ -53,13 +38,12 @@ func TestGetLiquidityPoolByID(t *testing.T) {
tt.Assert.Equal(lp.PoolID, resource.ID)
tt.Assert.Equal("constant_product", resource.Type)
tt.Assert.Equal(uint32(30), resource.FeeBP)
tt.Assert.Equal(uint64(100), resource.TotalTrustlines)
tt.Assert.Equal("200.0000000", resource.TotalShares)

tt.Assert.Equal(uint64(12345), resource.TotalTrustlines)
tt.Assert.Equal("0.0067890", resource.TotalShares)
tt.Assert.Equal("native", resource.Reserves[0].Asset)
tt.Assert.Equal("0.0000100", resource.Reserves[0].Amount)

tt.Assert.Equal("USD:GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", resource.Reserves[1].Asset)
tt.Assert.Equal(usdAsset.StringCanonical(), resource.Reserves[1].Asset)
tt.Assert.Equal("0.0000200", resource.Reserves[1].Amount)

// try to fetch pool which does not exist
Expand Down Expand Up @@ -92,42 +76,8 @@ func TestGetLiquidityPools(t *testing.T) {
test.ResetHorizonDB(t, tt.HorizonDB)
q := &history.Q{tt.HorizonSession()}

lp1 := history.LiquidityPool{
PoolID: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct,
Fee: 30,
TrustlineCount: 100,
ShareCount: 2000000000,
AssetReserves: history.LiquidityPoolAssetReserves{
{
Asset: xdr.MustNewNativeAsset(),
Reserve: 100,
},
{
Asset: xdr.MustNewCreditAsset("USD", "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"),
Reserve: 200,
},
},
LastModifiedLedger: 100,
}
lp2 := history.LiquidityPool{
PoolID: "d827bf10a721d217de3cd9ab3f10198a54de558c093a511ec426028618df2633",
Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct,
Fee: 30,
TrustlineCount: 300,
ShareCount: 4000000000,
AssetReserves: history.LiquidityPoolAssetReserves{
{
Asset: xdr.MustNewCreditAsset("EUR", "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"),
Reserve: 300,
},
{
Asset: xdr.MustNewCreditAsset("USD", "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"),
Reserve: 400,
},
},
LastModifiedLedger: 100,
}
lp1 := history.MakeTestPool(nativeAsset, 100, usdAsset, 200)
lp2 := history.MakeTestPool(eurAsset, 300, usdAsset, 400)
err := q.UpsertLiquidityPools(tt.Ctx, []history.LiquidityPool{lp1, lp2})
tt.Assert.NoError(err)

Expand All @@ -142,48 +92,79 @@ func TestGetLiquidityPools(t *testing.T) {
tt.Assert.Len(response, 2)

resource := response[0].(protocol.LiquidityPool)
tt.Assert.Equal("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", resource.ID)
tt.Assert.Equal(lp1.PoolID, resource.ID)
tt.Assert.Equal("constant_product", resource.Type)
tt.Assert.Equal(uint32(30), resource.FeeBP)
tt.Assert.Equal(uint64(100), resource.TotalTrustlines)
tt.Assert.Equal("200.0000000", resource.TotalShares)
tt.Assert.Equal(uint64(12345), resource.TotalTrustlines)
tt.Assert.Equal("0.0067890", resource.TotalShares)

tt.Assert.Equal("native", resource.Reserves[0].Asset)
tt.Assert.Equal("0.0000100", resource.Reserves[0].Amount)

tt.Assert.Equal("USD:GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", resource.Reserves[1].Asset)
tt.Assert.Equal(usdAsset.StringCanonical(), resource.Reserves[1].Asset)
tt.Assert.Equal("0.0000200", resource.Reserves[1].Amount)

resource = response[1].(protocol.LiquidityPool)
tt.Assert.Equal("d827bf10a721d217de3cd9ab3f10198a54de558c093a511ec426028618df2633", resource.ID)
tt.Assert.Equal(lp2.PoolID, resource.ID)
tt.Assert.Equal("constant_product", resource.Type)
tt.Assert.Equal(uint32(30), resource.FeeBP)
tt.Assert.Equal(uint64(300), resource.TotalTrustlines)
tt.Assert.Equal("400.0000000", resource.TotalShares)
tt.Assert.Equal(uint64(12345), resource.TotalTrustlines)
tt.Assert.Equal("0.0067890", resource.TotalShares)

tt.Assert.Equal("EUR:GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", resource.Reserves[0].Asset)
tt.Assert.Equal(eurAsset.StringCanonical(), resource.Reserves[0].Asset)
tt.Assert.Equal("0.0000300", resource.Reserves[0].Amount)

tt.Assert.Equal("USD:GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", resource.Reserves[1].Asset)
tt.Assert.Equal(usdAsset.StringCanonical(), resource.Reserves[1].Asset)
tt.Assert.Equal("0.0000400", resource.Reserves[1].Amount)

response, err = handler.GetResourcePage(httptest.NewRecorder(), makeRequest(
t,
map[string]string{"reserves": "native"},
map[string]string{},
q,
))
tt.Assert.NoError(err)
tt.Assert.Len(response, 1)

response, err = handler.GetResourcePage(httptest.NewRecorder(), makeRequest(
t,
map[string]string{"cursor": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
map[string]string{},
q,
))
tt.Assert.NoError(err)
tt.Assert.Len(response, 1)
resource = response[0].(protocol.LiquidityPool)
tt.Assert.Equal("d827bf10a721d217de3cd9ab3f10198a54de558c093a511ec426028618df2633", resource.ID)
t.Run("filtering by reserves", func(t *testing.T) {
response, err = handler.GetResourcePage(httptest.NewRecorder(), makeRequest(
t,
map[string]string{"reserves": "native"},
map[string]string{},
q,
))
assert.NoError(t, err)
assert.Len(t, response, 1)
})

t.Run("paging via cursor", func(t *testing.T) {
response, err = handler.GetResourcePage(httptest.NewRecorder(), makeRequest(
t,
map[string]string{"cursor": lp1.PoolID},
map[string]string{},
q,
))
assert.NoError(t, err)
assert.Len(t, response, 1)
resource = response[0].(protocol.LiquidityPool)
assert.Equal(t, lp2.PoolID, resource.ID)
})

t.Run("filtering by participating account", func(t *testing.T) {
// we need to add trustlines to filter by account
accountId := keypair.MustRandom().Address()

for _, tl := range []history.TrustLine{
history.MakeTestTrustline(accountId, nativeAsset, ""),
history.MakeTestTrustline(accountId, eurAsset, ""),
history.MakeTestTrustline(accountId, xdr.Asset{}, lp1.PoolID),
} {
err = q.UpsertTrustLines(tt.Ctx, []history.TrustLine{tl})
assert.NoError(t, err)
}

request := makeRequest(
t,
map[string]string{"account": accountId},
map[string]string{},
q,
)
assert.Contains(t, request.URL.String(), fmt.Sprintf("account=%s", accountId))

handler := GetLiquidityPoolsHandler{}
response, err := handler.GetResourcePage(httptest.NewRecorder(), request)
assert.NoError(t, err)
assert.Len(t, response, 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth checking the actual response if the LP is the one we expect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check has been added.

})
}
97 changes: 96 additions & 1 deletion services/horizon/internal/db2/history/liquidity_pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import (
"context"
"database/sql/driver"
"encoding/json"
"fmt"
"strings"

sq "github.com/Masterminds/squirrel"
"github.com/guregu/null"
"github.com/stellar/go/services/horizon/internal/db2"
"github.com/stellar/go/support/errors"
"github.com/stellar/go/support/log"
"github.com/stellar/go/xdr"
)

// LiquidityPoolsQuery is a helper struct to configure queries to liquidity pools
type LiquidityPoolsQuery struct {
PageQuery db2.PageQuery
Assets []xdr.Asset
AccountID string
}

// LiquidityPool is a row of data from the `liquidity_pools`.
Expand Down Expand Up @@ -150,8 +155,30 @@ func (q *Q) FindLiquidityPoolByID(ctx context.Context, liquidityPoolID string) (
return lp, err
}

// GetLiquidityPools finds all liquidity pools where accountID is one of the claimants
// findLiquidityPoolsByAccountId finds all liquidity pools that accountID is a participant of
func (q *Q) findLiquidityPoolsByAccountId(ctx context.Context, accountID string) ([]LiquidityPool, error) {
var results []LiquidityPool
sql := selectLiquidityPoolsJoinedTrustlines.Where("lp.deleted = ?", false)
sql = sql.Where("trust_lines.account_id = ?", accountID)
if err := q.Select(ctx, &results, sql); err != nil {
return nil, errors.Wrap(err, "could not run select join query")
}

return results, nil
}

// GetLiquidityPools finds all liquidity pools where accountID owns assets
func (q *Q) GetLiquidityPools(ctx context.Context, query LiquidityPoolsQuery) ([]LiquidityPool, error) {
log.Infof("%+v", query)
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
if len(query.AccountID) > 0 && len(query.Assets) > 0 {
return nil, fmt.Errorf("only one of `account id` or `assets` can be specified in a liquidity pool request")
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
}

if len(query.AccountID) > 0 {
log.Infof("find by account id")
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
return q.findLiquidityPoolsByAccountId(ctx, query.AccountID)
}

sql, err := query.PageQuery.ApplyRawTo(selectLiquidityPools, "lp.id")
if err != nil {
return nil, errors.Wrap(err, "could not apply query to page")
Expand Down Expand Up @@ -219,3 +246,71 @@ var liquidityPoolsSelectStatement = "lp.id, " +
"lp.last_modified_ledger"

var selectLiquidityPools = sq.Select(liquidityPoolsSelectStatement).From("liquidity_pools lp")
var selectLiquidityPoolsJoinedTrustlines = selectLiquidityPools.LeftJoin("trust_lines ON id = liquidity_pool_id")

// MakeTestPool is a helper to make liquidity pools for testing purposes. It's
// public because it's used in other test suites.
func MakeTestPool(A xdr.Asset, a uint64, B xdr.Asset, b uint64) LiquidityPool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note for reviewers: these are intentionally public as they're used both in these tests and in the actions' tests.

if !A.LessThan(B) {
B, A = A, B
b, a = a, b
}

poolId, _ := xdr.NewPoolId(A, B, xdr.LiquidityPoolFeeV18)
hexPoolId, _ := xdr.MarshalHex(poolId)
return LiquidityPool{
PoolID: hexPoolId,
Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct,
Fee: xdr.LiquidityPoolFeeV18,
TrustlineCount: 12345,
ShareCount: 67890,
AssetReserves: []LiquidityPoolAssetReserve{
{Asset: A, Reserve: a},
{Asset: B, Reserve: b},
},
LastModifiedLedger: 123,
}
}

func MakeTestTrustline(account string, asset xdr.Asset, poolId string) TrustLine {
trustline := TrustLine{
AccountID: account,
Balance: 1000,
AssetCode: "",
AssetIssuer: "",
LedgerKey: "irrelevant",
LiquidityPoolID: poolId,
Flags: 0,
LastModifiedLedger: 1234,
Sponsor: null.String{},
}

if poolId == "" {
trustline.AssetType = asset.Type
switch asset.Type {
case xdr.AssetTypeAssetTypeNative:
trustline.AssetCode = "native"

case xdr.AssetTypeAssetTypeCreditAlphanum4:
fallthrough
case xdr.AssetTypeAssetTypeCreditAlphanum12:
fmt.Println("Code:", asset.GetCode())
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
trustline.AssetCode = strings.TrimRight(asset.GetCode(), "\x00") // no nulls in db string
fmt.Println("Code:", trustline.AssetCode)
trustline.AssetIssuer = asset.GetIssuer()
trustline.BuyingLiabilities = 1
trustline.SellingLiabilities = 1

default:
panic("invalid asset type")
}

trustline.Limit = trustline.Balance * 10
trustline.BuyingLiabilities = 1
trustline.SellingLiabilities = 2
} else {
trustline.AssetType = xdr.AssetTypeAssetTypePoolShare
}

return trustline
}
Loading