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: Allow filtering accounts by liquidity pool participation. #3873

Merged
merged 14 commits into from
Aug 31, 2021
Merged
4 changes: 3 additions & 1 deletion protocols/horizon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,11 @@ type AssetStatAccounts struct {
Unauthorized int32 `json:"unauthorized"`
}

// Balance represents an account's holdings for a single currency type
// Balance represents an account's holdings for either a single currency type or
// shares in a liquidity pool.
type Balance struct {
Balance string `json:"balance"`
LiquidityPoolId string `json:"liquidity_pool_id,omitempty"`
Limit string `json:"limit,omitempty"`
BuyingLiabilities string `json:"buying_liabilities,omitempty"`
SellingLiabilities string `json:"selling_liabilities,omitempty"`
Expand Down
10 changes: 9 additions & 1 deletion services/horizon/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ file. This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

* The `--ingest` flag is set by default. If `--captive-core-config-path` is not set, the config file is generated based on network passhprase. ([3783](https://github.com/stellar/go/pull/3783))
### Breaking
* The `--ingest` flag is set by default. If `--captive-core-config-path` is not set, the config file is generated based on network passhprase ([3783](https://github.com/stellar/go/pull/3783)).

### Add
* Add a feature flag `--captive-core-reuse-storage-path`/`CAPTIVE_CORE_REUSE_STORAGE_PATH` that will reuse Captive Core's storage path for bucket files when applicable for better performance ([3750](https://github.com/stellar/go/pull/3750)).

* Add the ability to filter accounts by their participation in a particular liquidity pool ([3873](https://github.com/stellar/go/pull/3873)).

### Update
* Include pool shares in account balances ([3873](https://github.com/stellar/go/pull/3873)).

## v2.6.1

**Upgrading to this version from <= v2.1.1 will trigger a state rebuild. During this process (which will take at least 10 minutes), Horizon will not ingest new ledgers.**
Expand Down
17 changes: 12 additions & 5 deletions services/horizon/internal/actions/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ func AccountInfo(ctx context.Context, hq *history.Q, addr string) (*protocol.Acc

// AccountsQuery query struct for accounts end-point
type AccountsQuery struct {
Signer string `schema:"signer" valid:"accountID,optional"`
Sponsor string `schema:"sponsor" valid:"accountID,optional"`
AssetFilter string `schema:"asset" valid:"asset,optional"`
Signer string `schema:"signer" valid:"accountID,optional"`
Sponsor string `schema:"sponsor" valid:"accountID,optional"`
AssetFilter string `schema:"asset" valid:"asset,optional"`
LiquidityPool string `schema:"liquidity_pool" valid:"liquidity_pool,optional"`
}

// URITemplate returns a rfc6570 URI template the query struct
Expand All @@ -83,7 +84,7 @@ var invalidAccountsParams = problem.P{
Type: "invalid_accounts_params",
Title: "Invalid Accounts Parameters",
Status: http.StatusBadRequest,
Detail: "Exactly one filter is required. Please ensure that you are including a signer, an asset, or a sponsor filter.",
Detail: "Exactly one filter is required. Please ensure that you are including a signer, sponsor, asset, or liquidity pool filter.",
}

// Validate runs custom validations.
Expand Down Expand Up @@ -124,7 +125,8 @@ type GetAccountsHandler struct {
}

// GetResourcePage returns a page containing the account records that have
// `signer` as a signer or have a trustline to the given asset.
// `signer` as a signer, `sponsor` as a sponsor, a trustline to the given
// `asset`, or participate in a particular `liquidity_pool`.
func (handler GetAccountsHandler) GetResourcePage(
w HeaderWriter,
r *http.Request,
Expand Down Expand Up @@ -158,6 +160,11 @@ func (handler GetAccountsHandler) GetResourcePage(
if err != nil {
return nil, errors.Wrap(err, "loading account records")
}
} else if len(qp.LiquidityPool) > 0 {
records, err = historyQ.AccountEntriesForLiquidityPool(ctx, qp.LiquidityPool, pq)
if err != nil {
return nil, errors.Wrap(err, "loading account records")
}
} else {
records, err = historyQ.AccountsForAsset(ctx, *qp.Asset(), pq)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions services/horizon/internal/actions/account_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package actions

import (
"github.com/guregu/null"
"net/http/httptest"
"testing"
"time"

"github.com/guregu/null"

"github.com/stretchr/testify/assert"

protocol "github.com/stellar/go/protocols/horizon"
Expand Down Expand Up @@ -622,7 +623,7 @@ func TestGetAccountsHandlerInvalidParams(t *testing.T) {

func TestAccountQueryURLTemplate(t *testing.T) {
tt := assert.New(t)
expected := "/accounts{?signer,sponsor,asset,cursor,limit,order}"
expected := "/accounts{?signer,sponsor,asset,liquidity_pool,cursor,limit,order}"
accountsQuery := AccountsQuery{}
tt.Equal(expected, accountsQuery.URITemplate())
}
2 changes: 1 addition & 1 deletion services/horizon/internal/actions_root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestRootAction(t *testing.T) {
err = json.Unmarshal(w.Body.Bytes(), &actual)
ht.Require.NoError(err)
ht.Assert.Equal(
"http://localhost/accounts{?signer,sponsor,asset,cursor,limit,order}",
"http://localhost/accounts{?signer,sponsor,asset,liquidity_pool,cursor,limit,order}",
actual.Links.Accounts.Href,
)
ht.Assert.Equal(
Expand Down
13 changes: 7 additions & 6 deletions services/horizon/internal/assets/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ import (
)

// ErrInvalidString gets returns when the string form of the asset type is invalid
var ErrInvalidString = errors.New("invalid asset type: was not one of 'native', 'credit_alphanum4', 'credit_alphanum12'")
var ErrInvalidString = errors.New("invalid asset type: was not one of 'native', 'credit_alphanum4', 'credit_alphanum12', 'liquidity_pool_shares'")

//ErrInvalidValue gets returned when the xdr.AssetType int value is not one of the valid enum values
var ErrInvalidValue = errors.New("unknown asset type, cannot convert to string")

// AssetTypeMap is the read-only (i.e. don't modify it) map from string names to xdr.AssetType
// values
// AssetTypeMap is the read-only (i.e. don't modify it) map from string names to
// xdr.AssetType values
var AssetTypeMap = map[string]xdr.AssetType{
"native": xdr.AssetTypeAssetTypeNative,
"credit_alphanum4": xdr.AssetTypeAssetTypeCreditAlphanum4,
"credit_alphanum12": xdr.AssetTypeAssetTypeCreditAlphanum12,
"native": xdr.AssetTypeAssetTypeNative,
"credit_alphanum4": xdr.AssetTypeAssetTypeCreditAlphanum4,
"credit_alphanum12": xdr.AssetTypeAssetTypeCreditAlphanum12,
"liquidity_pool_shares": xdr.AssetTypeAssetTypePoolShare,
Copy link
Contributor

Choose a reason for hiding this comment

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

AssetTypeMap is used to validate asset query parameters for a bunch of different endpoints in horizon including the trades and path finding endpoints. Not all horizon endpoints should accept pool share asset types. For example pool shares are not transferrable therefore it doesn't make sense to do path payments where the source or destination asset is a pool share.

I think we should treat the endpoints which accept pool shares asset parameters as a special case rather than making all asset parameters validate pool shares as ok

Copy link
Contributor

Choose a reason for hiding this comment

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

I removed it in 1b8eeb9 however I think AssetTypeMap should be a general usage map that's mapping all types. If there are actions that doesn't allow liquidity pools we should probably add an extra validation there. We should probably do it after future net deploy.

}

//Parse creates an asset from the provided strings. See AssetTypeMap for valid strings for aType.
Expand Down
26 changes: 26 additions & 0 deletions services/horizon/internal/db2/history/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,32 @@ func (q *Q) AccountEntriesForSigner(ctx context.Context, signer string, page db2
return results, nil
}

// AccountEntriesForLiquidityPool returns a list of `AccountEntry` rows that
// have trustlines established with a given liquidity pool ID.
func (q *Q) AccountEntriesForLiquidityPool(ctx context.Context, poolId string, page db2.PageQuery) ([]AccountEntry, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

we already have AccountsForLiquidityPool(), do we need to add this function?

Copy link
Contributor

Choose a reason for hiding this comment

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

Fixed in 1b8eeb9.

return nil, errors.New("filtering by liquidity pool is not implemented")

sql := sq.
Select("accounts.*").
From("accounts").
Join("trust_lines ON accounts.account_id = trust_lines.account_id").
Where(map[string]interface{}{
"trust_lines.liquidity_pool_id": poolId,
})

sql, err := page.ApplyToUsingCursor(sql, "trust_lines.liquidity_pool_id", page.Cursor)
if err != nil {
return nil, errors.Wrap(err, "could not apply query to page")
}

var results []AccountEntry
if err := q.Select(ctx, &results, sql); err != nil {
return nil, errors.Wrap(err, "could not run select query")
}

return results, nil
}

var selectAccounts = sq.Select(`
account_id,
balance,
Expand Down
10 changes: 9 additions & 1 deletion services/horizon/internal/resourceadapter/account_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ func PopulateAccountEntry(
// populate balances
dest.Balances = make([]protocol.Balance, len(trustLines)+1)
for i, tl := range trustLines {
err := PopulateBalance(&dest.Balances[i], tl)
var err error

switch tl.AssetType {
case xdr.AssetTypeAssetTypePoolShare:
err = PopulatePoolShareBalance(&dest.Balances[i], tl)
default:
err = PopulateAssetBalance(&dest.Balances[i], tl)
}

if err != nil {
return errors.Wrap(err, "populating balance")
}
Expand Down
34 changes: 27 additions & 7 deletions services/horizon/internal/resourceadapter/account_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package resourceadapter
import (
"encoding/base64"
"encoding/json"
"github.com/guregu/null"
"strconv"
"testing"
"time"

"github.com/guregu/null"

"github.com/stellar/go/amount"
. "github.com/stellar/go/protocols/horizon"
protocol "github.com/stellar/go/protocols/horizon"
Expand Down Expand Up @@ -75,7 +76,7 @@ var (
AccountID: accountID.Address(),
AssetCode: "EUR",
AssetIssuer: trustLineIssuer.Address(),
AssetType: 1,
AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4,
Balance: 20000,
Limit: 223456789,
Flags: 1,
Expand All @@ -85,16 +86,24 @@ var (
},
{
AccountID: accountID.Address(),
AssetCode: "USD",
AssetCode: "USDDDDDDDDDD",
AssetIssuer: trustLineIssuer.Address(),
AssetType: 1,
AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12,
Balance: 10000,
Limit: 123456789,
Flags: 0,
SellingLiabilities: 2,
BuyingLiabilities: 1,
LastModifiedLedger: 900,
},
{
AccountID: accountID.Address(),
AssetType: xdr.AssetTypeAssetTypePoolShare,
Balance: 10000,
Limit: 123456789,
Flags: 0,
LastModifiedLedger: 900,
},
}

signers = []history.AccountSigner{
Expand Down Expand Up @@ -166,7 +175,7 @@ func TestPopulateAccountEntry(t *testing.T) {
tt.Equal(d.Value, history.AccountDataValue(want))
}

tt.Len(hAccount.Balances, 3)
tt.Len(hAccount.Balances, 4)

for i, t := range trustLines {
ht := hAccount.Balances[i]
Expand All @@ -177,8 +186,19 @@ func TestPopulateAccountEntry(t *testing.T) {
tt.Equal(wantType, ht.Type)

tt.Equal(amount.StringFromInt64(t.Balance), ht.Balance)
tt.Equal(amount.StringFromInt64(t.BuyingLiabilities), ht.BuyingLiabilities)
tt.Equal(amount.StringFromInt64(t.SellingLiabilities), ht.SellingLiabilities)

wantBuy := ""
if t.BuyingLiabilities != 0 {
wantBuy = amount.StringFromInt64(t.BuyingLiabilities)
}
tt.Equal(wantBuy, ht.BuyingLiabilities)

wantSell := ""
if t.SellingLiabilities != 0 {
wantSell = amount.StringFromInt64(t.SellingLiabilities)
}
tt.Equal(wantSell, ht.SellingLiabilities)

tt.Equal(amount.StringFromInt64(t.Limit), ht.Limit)
tt.Equal(t.LastModifiedLedger, ht.LastModifiedLedger)
tt.Equal(t.IsAuthorized(), *ht.IsAuthorized)
Expand Down
53 changes: 40 additions & 13 deletions services/horizon/internal/resourceadapter/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,27 @@ import (
"github.com/stellar/go/xdr"
)

func PopulateBalance(dest *protocol.Balance, row history.TrustLine) (err error) {
func PopulatePoolShareBalance(dest *protocol.Balance, row history.TrustLine) (err error) {
dest.Type, err = assets.String(row.AssetType)
if err != nil {
return errors.Wrap(err, "getting the string representation from the provided xdr asset type")
return err
}
if dest.Type != "liquidity_pool_shares" {
return PopulateAssetBalance(dest, row)
}

dest.Balance = amount.StringFromInt64(row.Balance)
dest.Limit = amount.StringFromInt64(row.Limit)
dest.LastModifiedLedger = row.LastModifiedLedger
fillAuthorizationFlags(dest, row)

return
}

func PopulateAssetBalance(dest *protocol.Balance, row history.TrustLine) (err error) {
dest.Type, err = assets.String(row.AssetType)
if err != nil {
return err
}

dest.Balance = amount.StringFromInt64(row.Balance)
Expand All @@ -22,20 +39,11 @@ func PopulateBalance(dest *protocol.Balance, row history.TrustLine) (err error)
dest.Issuer = row.AssetIssuer
dest.Code = row.AssetCode
dest.LastModifiedLedger = row.LastModifiedLedger
isAuthorized := row.IsAuthorized()
dest.IsAuthorized = &isAuthorized
dest.IsAuthorizedToMaintainLiabilities = &isAuthorized
isAuthorizedToMaintainLiabilities := row.IsAuthorizedToMaintainLiabilities()
if isAuthorizedToMaintainLiabilities {
dest.IsAuthorizedToMaintainLiabilities = &isAuthorizedToMaintainLiabilities
}
isClawbackEnabled := row.IsClawbackEnabled()
if isClawbackEnabled {
dest.IsClawbackEnabled = &isClawbackEnabled
}
fillAuthorizationFlags(dest, row)
if row.Sponsor.Valid {
dest.Sponsor = row.Sponsor.String
}

return
}

Expand All @@ -54,5 +62,24 @@ func PopulateNativeBalance(dest *protocol.Balance, stroops, buyingLiabilities, s
dest.Code = ""
dest.IsAuthorized = nil
dest.IsAuthorizedToMaintainLiabilities = nil
dest.IsClawbackEnabled = nil
return
}

func fillAuthorizationFlags(dest *protocol.Balance, row history.TrustLine) {
isAuthorized := row.IsAuthorized()
dest.IsAuthorized = &isAuthorized

// After CAP-18, isAuth => isAuthToMaintain, so the following code does this
// in a backwards compatible manner.
dest.IsAuthorizedToMaintainLiabilities = &isAuthorized
isAuthorizedToMaintainLiabilities := row.IsAuthorizedToMaintainLiabilities()
if isAuthorizedToMaintainLiabilities {
dest.IsAuthorizedToMaintainLiabilities = &isAuthorizedToMaintainLiabilities
}

isClawbackEnabled := row.IsClawbackEnabled()
if isClawbackEnabled {
dest.IsClawbackEnabled = &isClawbackEnabled
}
}
24 changes: 21 additions & 3 deletions services/horizon/internal/resourceadapter/balance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,16 @@ func TestPopulateBalance(t *testing.T) {
Balance: 10,
Flags: 0,
}
poolshareTrustline := history.TrustLine{
AccountID: "testID",
AssetType: xdr.AssetTypeAssetTypePoolShare,
Limit: 100,
Balance: 10,
Flags: 0,
}

want := Balance{}
err := PopulateBalance(&want, authorizedTrustline)
err := PopulateAssetBalance(&want, authorizedTrustline)
assert.NoError(t, err)
assert.Equal(t, "credit_alphanum12", want.Type)
assert.Equal(t, "0.0000010", want.Balance)
Expand All @@ -52,7 +59,7 @@ func TestPopulateBalance(t *testing.T) {
assert.Equal(t, true, *want.IsAuthorizedToMaintainLiabilities)

want = Balance{}
err = PopulateBalance(&want, authorizedToMaintainLiabilitiesTrustline)
err = PopulateAssetBalance(&want, authorizedToMaintainLiabilitiesTrustline)
assert.NoError(t, err)
assert.Equal(t, "credit_alphanum12", want.Type)
assert.Equal(t, "0.0000010", want.Balance)
Expand All @@ -63,7 +70,7 @@ func TestPopulateBalance(t *testing.T) {
assert.Equal(t, true, *want.IsAuthorizedToMaintainLiabilities)

want = Balance{}
err = PopulateBalance(&want, unauthorizedTrustline)
err = PopulateAssetBalance(&want, unauthorizedTrustline)
assert.NoError(t, err)
assert.Equal(t, "credit_alphanum12", want.Type)
assert.Equal(t, "0.0000010", want.Balance)
Expand All @@ -72,6 +79,17 @@ func TestPopulateBalance(t *testing.T) {
assert.Equal(t, testAssetCode2, want.Code)
assert.Equal(t, false, *want.IsAuthorized)
assert.Equal(t, false, *want.IsAuthorizedToMaintainLiabilities)

want = Balance{}
err = PopulatePoolShareBalance(&want, poolshareTrustline)
assert.NoError(t, err)
assert.Equal(t, "liquidity_pool_shares", want.Type)
assert.Equal(t, "0.0000010", want.Balance)
assert.Equal(t, "0.0000100", want.Limit)
assert.Equal(t, "", want.Issuer)
assert.Equal(t, "", want.Code)
assert.Equal(t, false, *want.IsAuthorized)
assert.Equal(t, false, *want.IsAuthorizedToMaintainLiabilities)
}

func TestPopulateNativeBalance(t *testing.T) {
Expand Down