diff --git a/services/horizon/internal/actions/asset.go b/services/horizon/internal/actions/asset.go index 1571f12e3d..785e74411f 100644 --- a/services/horizon/internal/actions/asset.go +++ b/services/horizon/internal/actions/asset.go @@ -39,15 +39,15 @@ func (handler AssetStatsHandler) validateAssetParams(code, issuer string, pq db2 } if pq.Cursor != "" { - parts := strings.Split(pq.Cursor, ":") - if len(parts) != 2 { + parts := strings.SplitN(pq.Cursor, "_", 3) + if len(parts) != 3 { return problem.MakeInvalidFieldProblem( "cursor", errors.New("cursor must contain exactly one colon"), ) } - cursorCode, cursorIssuer := parts[0], parts[1] + cursorCode, cursorIssuer, assetType := parts[0], parts[1], parts[2] if !xdr.ValidAssetCode.MatchString(cursorCode) { return problem.MakeInvalidFieldProblem( "cursor", @@ -61,6 +61,14 @@ func (handler AssetStatsHandler) validateAssetParams(code, issuer string, pq db2 fmt.Errorf("%s is not a valid asset issuer", cursorIssuer), ) } + + if _, ok := xdr.StringToAssetType[assetType]; !ok { + return problem.MakeInvalidFieldProblem( + "cursor", + fmt.Errorf("%s is not a valid asset type", assetType), + ) + } + } return nil diff --git a/services/horizon/internal/actions/asset_test.go b/services/horizon/internal/actions/asset_test.go index 2985474fef..25985a2dc3 100644 --- a/services/horizon/internal/actions/asset_test.go +++ b/services/horizon/internal/actions/asset_test.go @@ -40,17 +40,17 @@ func TestAssetStatsValidation(t *testing.T) { "not a valid asset issuer", }, { - "cursor has too many colons", + "cursor has too many underscores", map[string]string{ - "cursor": "ABC:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H:", + "cursor": "ABC_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4_", }, "cursor", - "cursor must contain exactly one colon", + "credit_alphanum4_ is not a valid asset type", }, { "invalid cursor code", map[string]string{ - "cursor": "tooooooooolong:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + "cursor": "tooooooooolong_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum12", }, "cursor", "not a valid asset code", @@ -58,11 +58,19 @@ func TestAssetStatsValidation(t *testing.T) { { "invalid cursor issuer", map[string]string{ - "cursor": "ABC:invalidissuer", + "cursor": "ABC_invalidissuer_credit_alphanum4", }, "cursor", "not a valid asset issuer", }, + { + "invalid cursor type", + map[string]string{ + "cursor": "ABC_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum123", + }, + "cursor", + "credit_alphanum123 is not a valid asset type", + }, } { t.Run(testCase.name, func(t *testing.T) { r := makeRequest(t, testCase.queryParams, map[string]string{}, nil) @@ -124,7 +132,7 @@ func TestAssetStats(t *testing.T) { Code: usdAssetStat.AssetCode, Issuer: usdAssetStat.AssetIssuer, }, - PT: usdAssetStat.AssetCode + ":" + usdAssetStat.AssetIssuer, + PT: usdAssetStat.PagingToken(), Flags: issuerFlags, } @@ -143,7 +151,7 @@ func TestAssetStats(t *testing.T) { Code: etherAssetStat.AssetCode, Issuer: etherAssetStat.AssetIssuer, }, - PT: etherAssetStat.AssetCode + ":" + etherAssetStat.AssetIssuer, + PT: etherAssetStat.PagingToken(), Flags: issuerFlags, } @@ -162,7 +170,7 @@ func TestAssetStats(t *testing.T) { Code: otherUSDAssetStat.AssetCode, Issuer: otherUSDAssetStat.AssetIssuer, }, - PT: otherUSDAssetStat.AssetCode + ":" + otherUSDAssetStat.AssetIssuer, + PT: otherUSDAssetStat.PagingToken(), } otherUSDAssetStatResponse.Links.Toml = hal.NewLink( "https://" + otherIssuer.HomeDomain + "/.well-known/stellar.toml", @@ -183,7 +191,7 @@ func TestAssetStats(t *testing.T) { Code: eurAssetStat.AssetCode, Issuer: eurAssetStat.AssetIssuer, }, - PT: eurAssetStat.AssetCode + ":" + eurAssetStat.AssetIssuer, + PT: eurAssetStat.PagingToken(), } eurAssetStatResponse.Links.Toml = hal.NewLink( "https://" + otherIssuer.HomeDomain + "/.well-known/stellar.toml", @@ -346,7 +354,7 @@ func TestAssetStatsIssuerDoesNotExist(t *testing.T) { Code: usdAssetStat.AssetCode, Issuer: usdAssetStat.AssetIssuer, }, - PT: usdAssetStat.AssetCode + ":" + usdAssetStat.AssetIssuer, + PT: usdAssetStat.PagingToken(), } tt.Assert.Len(results, 1) diff --git a/services/horizon/internal/db2/history/asset_stats.go b/services/horizon/internal/db2/history/asset_stats.go index 03f5a28297..ec914ede7a 100644 --- a/services/horizon/internal/db2/history/asset_stats.go +++ b/services/horizon/internal/db2/history/asset_stats.go @@ -104,12 +104,12 @@ func (q *Q) GetAssetStat(assetType xdr.AssetType, assetCode, assetIssuer string) } func parseAssetStatsCursor(cursor string) (string, string, error) { - parts := strings.Split(cursor, ":") - if len(parts) != 2 { + parts := strings.SplitN(cursor, "_", 3) + if len(parts) != 3 { return "", "", fmt.Errorf("invalid asset stats cursor: %v", cursor) } - code, issuer := parts[0], parts[1] + code, issuer, assetType := parts[0], parts[1], parts[2] var issuerAccount xdr.AccountId var asset xdr.Asset @@ -127,6 +127,10 @@ func parseAssetStatsCursor(cursor string) (string, string, error) { ) } + if _, ok := xdr.StringToAssetType[assetType]; !ok { + return "", "", errors.Errorf("invalid asset type in asset stats cursor: %v", cursor) + } + return code, issuer, nil } diff --git a/services/horizon/internal/db2/history/asset_stats_test.go b/services/horizon/internal/db2/history/asset_stats_test.go index 1b32662d24..7924a3f916 100644 --- a/services/horizon/internal/db2/history/asset_stats_test.go +++ b/services/horizon/internal/db2/history/asset_stats_test.go @@ -241,28 +241,33 @@ func TestGetAssetStatsCursorValidation(t *testing.T) { expectedError string }{ { - "cursor does not use colon as serpator", + "cursor does not use underscore as serpator", "usdc-GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", "invalid asset stats cursor", }, { - "cursor has no colon", + "cursor has no underscore", "usdc", "invalid asset stats cursor", }, { - "cursor has too many colons", - "usdc:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H:", - "invalid asset stats cursor", + "cursor has too many underscores", + "usdc_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4_", + "invalid asset type in asset stats cursor", }, { "issuer in cursor is invalid", - "usd:abcdefghijklmnopqrstuv", + "usd_abcdefghijklmnopqrstuv_credit_alphanum4", "invalid issuer in asset stats cursor", }, + { + "asset type in cursor is invalid", + "usd_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum", + "invalid asset type in asset stats cursor", + }, { "asset code in cursor is too long", - "abcdefghijklmnopqrstuv:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + "abcdefghijklmnopqrstuv_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum12", "invalid asset stats cursor", }, } { @@ -376,7 +381,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "no filter with cursor", "", "", - "ABC:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "ABC_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", []ExpAssetStat{ etherAssetStat, @@ -389,7 +394,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "no filter with cursor descending", "", "", - "ZZZ:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + "ZZZ_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4", "desc", []ExpAssetStat{ usdAssetStat, @@ -402,7 +407,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "no filter with cursor and offset", "", "", - "ETHER:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + "ETHER_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum12", "asc", []ExpAssetStat{ eurAssetStat, @@ -414,7 +419,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "no filter with cursor and offset descending", "", "", - "EUR:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "EUR_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "desc", []ExpAssetStat{ etherAssetStat, @@ -424,7 +429,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "no filter with cursor and offset descending including eur", "", "", - "EUR:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + "EUR_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4", "desc", []ExpAssetStat{ eurAssetStat, @@ -446,7 +451,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "filter on code with cursor", "USD", "", - "USD:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "USD_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", []ExpAssetStat{ usdAssetStat, @@ -456,7 +461,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "filter on code with cursor descending", "USD", "", - "USD:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + "USD_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4", "desc", []ExpAssetStat{ otherUSDAssetStat, @@ -477,7 +482,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "filter on issuer with cursor", "", "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", - "EUR:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "EUR_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", []ExpAssetStat{ otherUSDAssetStat, @@ -487,7 +492,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "filter on issuer with cursor descending", "", "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", - "USD:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "USD_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "desc", []ExpAssetStat{ eurAssetStat, @@ -505,7 +510,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "filter on non existant code with cursor", "BTC", "", - "BTC:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "BTC_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", nil, }, @@ -521,7 +526,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "filter on non existant issuer with cursor", "", "GAEIHD6U4WSBHJGA2HPWOQ3OQEFQ3Y7QZE2DR76YKZNKPW5YDLYW4UGF", - "AAA:GAEIHD6U4WSBHJGA2HPWOQ3OQEFQ3Y7QZE2DR76YKZNKPW5YDLYW4UGF", + "AAA_GAEIHD6U4WSBHJGA2HPWOQ3OQEFQ3Y7QZE2DR76YKZNKPW5YDLYW4UGF_credit_alphanum4", "asc", nil, }, @@ -537,7 +542,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "filter on non existant code and non existant issuer with cursor", "BTC", "GAEIHD6U4WSBHJGA2HPWOQ3OQEFQ3Y7QZE2DR76YKZNKPW5YDLYW4UGF", - "AAA:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "AAA_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", nil, }, @@ -555,7 +560,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "filter on both code and issuer with cursor", "USD", "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", - "USC:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "USC_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", []ExpAssetStat{ otherUSDAssetStat, @@ -565,7 +570,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "filter on both code and issuer with cursor descending", "USD", "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", - "USE:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "USE_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "desc", []ExpAssetStat{ otherUSDAssetStat, @@ -575,7 +580,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "cursor negates filter", "USD", "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", - "USD:GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "USD_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", nil, }, diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index 7aa2ef1ebf..333fc8ca19 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -242,7 +242,12 @@ type ExpAssetStat struct { // PagingToken returns a cursor for this asset stat func (e ExpAssetStat) PagingToken() string { - return fmt.Sprintf("%s:%s", e.AssetCode, e.AssetIssuer) + return fmt.Sprintf( + "%s_%s_%s", + e.AssetCode, + e.AssetIssuer, + xdr.AssetTypeToString[e.AssetType], + ) } // QAssetStats defines exp_asset_stats related queries. diff --git a/services/horizon/internal/expingest/processors/asset_stats_set.go b/services/horizon/internal/expingest/processors/asset_stats_set.go index c03b5a442d..ee12344136 100644 --- a/services/horizon/internal/expingest/processors/asset_stats_set.go +++ b/services/horizon/internal/expingest/processors/asset_stats_set.go @@ -22,7 +22,12 @@ type assetStatValue struct { type AssetStatSet map[assetStatKey]*assetStatValue // Add updates the set with a trustline entry from a history archive snapshot +// if the trustline is authorized func (s AssetStatSet) Add(trustLine xdr.TrustLineEntry) error { + if !xdr.TrustLineFlags(trustLine.Flags).IsAuthorized() { + return nil + } + var key assetStatKey if err := trustLine.Asset.Extract(&key.assetType, &key.assetCode, &key.assetIssuer); err != nil { return errors.Wrap(err, "could not extract asset info from trustline") diff --git a/services/horizon/internal/expingest/processors/asset_stats_set_test.go b/services/horizon/internal/expingest/processors/asset_stats_set_test.go index 873bdca687..ef842ec898 100644 --- a/services/horizon/internal/expingest/processors/asset_stats_set_test.go +++ b/services/horizon/internal/expingest/processors/asset_stats_set_test.go @@ -40,6 +40,21 @@ func assertAllEquals(t *testing.T, set AssetStatSet, expected []history.ExpAsset } } +func TestAssetStatSetIgnoresUnauthorizedTrustlines(t *testing.T) { + set := AssetStatSet{} + err := set.Add(xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 1, + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if all := set.All(); len(all) != 0 { + t.Fatalf("expected empty list but got %v", all) + } +} + func TestAddAndRemoveAssetStats(t *testing.T) { set := AssetStatSet{} eur := "EUR" @@ -55,6 +70,7 @@ func TestAddAndRemoveAssetStats(t *testing.T) { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset(eur, trustLineIssuer.Address()), Balance: 1, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), }) if err != nil { t.Fatalf("unexpected error %v", err) @@ -65,6 +81,7 @@ func TestAddAndRemoveAssetStats(t *testing.T) { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset(eur, trustLineIssuer.Address()), Balance: 24, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), }) if err != nil { t.Fatalf("unexpected error %v", err) @@ -79,6 +96,7 @@ func TestAddAndRemoveAssetStats(t *testing.T) { AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), Asset: xdr.MustNewCreditAsset(usd, trustLineIssuer.Address()), Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), }) if err != nil { t.Fatalf("unexpected error %v", err) @@ -89,6 +107,7 @@ func TestAddAndRemoveAssetStats(t *testing.T) { AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), Asset: xdr.MustNewCreditAsset(ether, trustLineIssuer.Address()), Balance: 3, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), }) if err != nil { t.Fatalf("unexpected error %v", err) @@ -133,6 +152,7 @@ func TestOverflowAssetStatSet(t *testing.T) { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset(eur, trustLineIssuer.Address()), Balance: math.MaxInt64, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), }) if err != nil { t.Fatalf("unexpected error %v", err) @@ -157,6 +177,7 @@ func TestOverflowAssetStatSet(t *testing.T) { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset(eur, trustLineIssuer.Address()), Balance: math.MaxInt64, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), }) if err != nil { t.Fatalf("unexpected error %v", err) diff --git a/services/horizon/internal/expingest/processors/database_processor.go b/services/horizon/internal/expingest/processors/database_processor.go index 8de89c3b52..04fd3076d8 100644 --- a/services/horizon/internal/expingest/processors/database_processor.go +++ b/services/horizon/internal/expingest/processors/database_processor.go @@ -488,10 +488,52 @@ func (p *DatabaseProcessor) processLedgerOffers(change io.Change) error { } func (p *DatabaseProcessor) adjustAssetStat( - trustline xdr.TrustLineEntry, - deltaBalance xdr.Int64, - deltaAccounts int32, + preTrustline *xdr.TrustLineEntry, + postTrustline *xdr.TrustLineEntry, ) error { + var deltaBalance xdr.Int64 + var deltaAccounts int32 + var trustline xdr.TrustLineEntry + + if preTrustline != nil && postTrustline == nil { + trustline = *preTrustline + // removing a trustline + if xdr.TrustLineFlags(preTrustline.Flags).IsAuthorized() { + deltaAccounts = -1 + deltaBalance = -preTrustline.Balance + } + } else if preTrustline == nil && postTrustline != nil { + trustline = *postTrustline + // adding a trustline + if xdr.TrustLineFlags(postTrustline.Flags).IsAuthorized() { + deltaAccounts = 1 + deltaBalance = postTrustline.Balance + } + } else if preTrustline != nil && postTrustline != nil { + trustline = *postTrustline + // updating a trustline + if xdr.TrustLineFlags(preTrustline.Flags).IsAuthorized() && + xdr.TrustLineFlags(postTrustline.Flags).IsAuthorized() { + // trustline remains authorized + deltaAccounts = 0 + deltaBalance = postTrustline.Balance - preTrustline.Balance + } else if xdr.TrustLineFlags(preTrustline.Flags).IsAuthorized() && + !xdr.TrustLineFlags(postTrustline.Flags).IsAuthorized() { + // trustline was authorized and became unauthorized + deltaAccounts = -1 + deltaBalance = -preTrustline.Balance + } else if !xdr.TrustLineFlags(preTrustline.Flags).IsAuthorized() && + xdr.TrustLineFlags(postTrustline.Flags).IsAuthorized() { + // trustline was unauthorized and became authorized + deltaAccounts = 1 + deltaBalance = postTrustline.Balance + } + // else, trustline was unauthorized and remains unauthorized + // so there is no change to accounts or balances + } else { + return verify.NewStateError(errors.New("both pre and post trustlines cannot be nil")) + } + if deltaBalance == 0 && deltaAccounts == 0 { return nil } @@ -611,7 +653,7 @@ func (p *DatabaseProcessor) processLedgerTrustLines(change io.Change) error { if err != nil { return errors.Wrap(err, "Error creating ledger key") } - err = p.adjustAssetStat(trustLine, trustLine.Balance, 1) + err = p.adjustAssetStat(nil, &trustLine) if err != nil { return errors.Wrap(err, "Error adjusting asset stat") } @@ -624,7 +666,7 @@ func (p *DatabaseProcessor) processLedgerTrustLines(change io.Change) error { if err != nil { return errors.Wrap(err, "Error creating ledger key") } - err = p.adjustAssetStat(trustLine, -trustLine.Balance, -1) + err = p.adjustAssetStat(&trustLine, nil) if err != nil { return errors.Wrap(err, "Error adjusting asset stat") } @@ -638,7 +680,7 @@ func (p *DatabaseProcessor) processLedgerTrustLines(change io.Change) error { if err != nil { return errors.Wrap(err, "Error creating ledger key") } - err = p.adjustAssetStat(trustLine, trustLine.Balance-preTrustLine.Balance, 0) + err = p.adjustAssetStat(&preTrustLine, &trustLine) if err != nil { return errors.Wrap(err, "Error adjusting asset stat") } diff --git a/services/horizon/internal/expingest/processors/trust_lines_processor_test.go b/services/horizon/internal/expingest/processors/trust_lines_processor_test.go index eb41a8c3b9..9bd0e7338b 100644 --- a/services/horizon/internal/expingest/processors/trust_lines_processor_test.go +++ b/services/horizon/internal/expingest/processors/trust_lines_processor_test.go @@ -61,10 +61,11 @@ func (s *TrustLinesProcessorTestSuiteState) TearDownTest() { s.mockStateWriter.AssertExpectations(s.T()) } -func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLine() { +func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLineWithAssetStat() { trustLine := xdr.TrustLineEntry{ AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), } lastModifiedLedgerSeq := xdr.Uint32(123) s.mockStateReader. @@ -111,6 +112,49 @@ func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLine() { s.Assert().NoError(err) } +func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLineWithoutAssetStat() { + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + } + lastModifiedLedgerSeq := xdr.Uint32(123) + s.mockStateReader. + On("Read").Return( + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + }, + }, + nil, + ).Once() + + s.mockBatchInsertBuilder. + On("Add", trustLine, lastModifiedLedgerSeq).Return(nil).Once() + + s.mockAssetStatsQ. + On("InsertAssetStats", []history.ExpAssetStat{}, maxBatchSize).Return(nil).Once() + + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{}, stdio.EOF).Once() + + s.mockBatchInsertBuilder.On("Exec").Return(nil).Once() + + err := s.processor.ProcessState( + context.Background(), + &supportPipeline.Store{}, + s.mockStateReader, + s.mockStateWriter, + ) + + s.Assert().NoError(err) +} + func TestTrustLinesProcessorTestSuiteLedger(t *testing.T) { suite.Run(t, new(TrustLinesProcessorTestSuiteLedger)) } @@ -188,6 +232,12 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + unauthorizedTrustline := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 0, } lastModifiedLedgerSeq := xdr.Uint32(1234) s.mockLedgerReader.On("Read"). @@ -195,7 +245,7 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { Meta: createTransactionMeta([]xdr.OperationMeta{ xdr.OperationMeta{ Changes: []xdr.LedgerEntryChange{ - // State + // Created xdr.LedgerEntryChange{ Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, Created: &xdr.LedgerEntry{ @@ -206,6 +256,16 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { }, }, }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &unauthorizedTrustline, + }, + }, + }, }, }, }), @@ -216,6 +276,11 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { trustLine, lastModifiedLedgerSeq, ).Return(int64(1), nil).Once() + s.mockQ.On( + "InsertTrustLine", + unauthorizedTrustline, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() s.mockAssetStatsQ.On("GetAssetStat", xdr.AssetTypeAssetTypeCreditAlphanum4, "EUR", @@ -233,6 +298,12 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + updatedUnauthorizedTrustline := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 10, } s.mockLedgerReader.On("Read"). Return(io.LedgerTransaction{ @@ -261,6 +332,28 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { }, }, }, + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &unauthorizedTrustline, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &updatedUnauthorizedTrustline, + }, + }, + }, }, }, }), @@ -270,6 +363,11 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { updatedTrustLine, lastModifiedLedgerSeq, ).Return(int64(1), nil).Once() + s.mockQ.On( + "UpdateTrustLine", + updatedUnauthorizedTrustline, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() s.mockAssetStatsQ.On("GetAssetStat", xdr.AssetTypeAssetTypeCreditAlphanum4, "EUR", @@ -314,11 +412,13 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestUpdateTrustLineNoRowsAffected() AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), } updatedTrustLine := xdr.TrustLineEntry{ AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), } s.mockLedgerReader.On("Read"). Return(io.LedgerTransaction{ @@ -387,7 +487,147 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestUpdateTrustLineNoRowsAffected() s.Assert().EqualError(err, "Error in TrustLines handler: No rows affected when updating trustline: GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB credit_alphanum4/EUR/GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") } +func (s *TrustLinesProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() { + lastModifiedLedgerSeq := xdr.Uint32(1234) + + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 100, + } + updatedTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + + otherTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 100, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + otherUpdatedTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 10, + } + + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &updatedTrustLine, + }, + }, + }, + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &otherTrustLine, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &otherUpdatedTrustLine, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "UpdateTrustLine", + updatedTrustLine, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() + s.mockAssetStatsQ.On("InsertAssetStat", history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "10", + NumAccounts: 1, + }).Return(int64(1), nil).Once() + + s.mockQ.On( + "UpdateTrustLine", + otherUpdatedTrustLine, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "USD", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "USD", + Amount: "100", + NumAccounts: 1, + }, nil).Once() + s.mockAssetStatsQ.On("RemoveAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "USD", + trustLineIssuer.Address(), + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveTrustLine() { + unauthorizedTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 0, + } + s.mockLedgerReader.On("Read"). Return(io.LedgerTransaction{ Meta: createTransactionMeta([]xdr.OperationMeta{ @@ -402,6 +642,7 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveTrustLine() { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), }, }, }, @@ -416,6 +657,25 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveTrustLine() { }, }, }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &unauthorizedTrustLine, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryRemoved, + Removed: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + }, + }, + }, }, }, }), @@ -428,6 +688,13 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveTrustLine() { Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), }, ).Return(int64(1), nil).Once() + s.mockQ.On( + "RemoveTrustLine", + xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + }, + ).Return(int64(1), nil).Once() s.mockAssetStatsQ.On("GetAssetStat", xdr.AssetTypeAssetTypeCreditAlphanum4, "EUR", @@ -468,6 +735,7 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestProcessUpgradeChange() { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), } lastModifiedLedgerSeq := xdr.Uint32(1234) s.mockLedgerReader.On("Read"). @@ -518,6 +786,7 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestProcessUpgradeChange() { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), } s.mockLedgerReader. @@ -581,7 +850,7 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestProcessUpgradeChange() { s.Assert().NoError(err) } -func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveOfferNoRowsAffected() { +func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveTrustlineNoRowsAffected() { // Removes ReadUpgradeChange assertion s.mockLedgerReader = &io.MockLedgerReader{} s.mockLedgerReader.On("Close").Return(nil).Once() @@ -600,6 +869,7 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveOfferNoRowsAffected() { AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), }, }, },