diff --git a/protocols/horizon/main.go b/protocols/horizon/main.go index e765371c5f..1cfc10bf9b 100644 --- a/protocols/horizon/main.go +++ b/protocols/horizon/main.go @@ -172,11 +172,13 @@ type AssetStat struct { NumAccounts int32 `json:"num_accounts"` NumClaimableBalances int32 `json:"num_claimable_balances"` NumLiquidityPools int32 `json:"num_liquidity_pools"` + NumContracts int32 `json:"num_contracts"` // Action needed in release: horizon-v3.0.0: deprecated field Amount string `json:"amount"` Accounts AssetStatAccounts `json:"accounts"` ClaimableBalancesAmount string `json:"claimable_balances_amount"` LiquidityPoolsAmount string `json:"liquidity_pools_amount"` + ContractsAmount string `json:"contracts_amount"` Balances AssetStatBalances `json:"balances"` Flags AccountFlags `json:"flags"` } diff --git a/services/horizon/internal/actions/asset_test.go b/services/horizon/internal/actions/asset_test.go index fcedf5985d..eb1a1df07d 100644 --- a/services/horizon/internal/actions/asset_test.go +++ b/services/horizon/internal/actions/asset_test.go @@ -159,6 +159,7 @@ func TestAssetStats(t *testing.T) { LiquidityPoolsAmount: "0.0000020", Amount: "0.0000001", NumAccounts: usdAssetStat.NumAccounts, + ContractsAmount: "0.0000000", Asset: base.Asset{ Type: "credit_alphanum4", Code: usdAssetStat.AssetCode, @@ -202,6 +203,7 @@ func TestAssetStats(t *testing.T) { }, ClaimableBalancesAmount: "0.0000000", LiquidityPoolsAmount: "0.0000000", + ContractsAmount: "0.0000000", Amount: "0.0000023", NumAccounts: etherAssetStat.NumAccounts, Asset: base.Asset{ @@ -248,6 +250,7 @@ func TestAssetStats(t *testing.T) { ClaimableBalancesAmount: "0.0000000", LiquidityPoolsAmount: "0.0000000", Amount: "0.0000001", + ContractsAmount: "0.0000000", NumAccounts: otherUSDAssetStat.NumAccounts, Asset: base.Asset{ Type: "credit_alphanum4", @@ -295,6 +298,7 @@ func TestAssetStats(t *testing.T) { ClaimableBalancesAmount: "0.0000000", LiquidityPoolsAmount: "0.0000000", Amount: "0.0000111", + ContractsAmount: "0.0000000", NumAccounts: eurAssetStat.NumAccounts, Asset: base.Asset{ Type: "credit_alphanum4", @@ -471,6 +475,7 @@ func TestAssetStatsIssuerDoesNotExist(t *testing.T) { ClaimableBalancesAmount: "0.0000000", LiquidityPoolsAmount: "0.0000000", Amount: "0.0000001", + ContractsAmount: "0.0000000", NumAccounts: usdAssetStat.NumAccounts, Asset: base.Asset{ Type: "credit_alphanum4", diff --git a/services/horizon/internal/db2/history/asset_stats.go b/services/horizon/internal/db2/history/asset_stats.go index cb329c9805..b13d913141 100644 --- a/services/horizon/internal/db2/history/asset_stats.go +++ b/services/horizon/internal/db2/history/asset_stats.go @@ -126,20 +126,6 @@ func (q *Q) GetAssetStatByContracts(ctx context.Context, contractIDs [][32]byte) return assetStats, err } -// CountContractIDs counts all rows in the asset stats table which have a contract id set. -// CountContractIDs is used by the state verification routine. -func (q *Q) CountContractIDs(ctx context.Context) (int, error) { - sql := sq.Select("count(*)").From("exp_asset_stats"). - Where("contract_id IS NOT NULL") - - var count int - if err := q.Get(ctx, &count, sql); err != nil { - return 0, errors.Wrap(err, "could not run select query") - } - - return count, nil -} - func parseAssetStatsCursor(cursor string) (string, string, error) { parts := strings.SplitN(cursor, "_", 3) if len(parts) != 3 { diff --git a/services/horizon/internal/db2/history/asset_stats_test.go b/services/horizon/internal/db2/history/asset_stats_test.go index 76bfaed5c0..ceeb962a05 100644 --- a/services/horizon/internal/db2/history/asset_stats_test.go +++ b/services/horizon/internal/db2/history/asset_stats_test.go @@ -16,11 +16,6 @@ func TestAssetStatContracts(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} - // asset stats is empty so count should be 0 - count, err := q.CountContractIDs(tt.Ctx) - tt.Assert.NoError(err) - tt.Assert.Equal(0, count) - assetStats := []ExpAssetStat{ { AssetType: xdr.AssetTypeAssetTypeNative, @@ -30,6 +25,7 @@ func TestAssetStatContracts(t *testing.T) { ClaimableBalances: 0, LiquidityPools: 0, Unauthorized: 0, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "0", @@ -37,6 +33,7 @@ func TestAssetStatContracts(t *testing.T) { ClaimableBalances: "0", LiquidityPools: "0", Unauthorized: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -49,6 +46,7 @@ func TestAssetStatContracts(t *testing.T) { Authorized: 1, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 7, }, Balances: ExpAssetStatBalances{ Authorized: "23", @@ -56,6 +54,7 @@ func TestAssetStatContracts(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "60", }, Amount: "23", NumAccounts: 1, @@ -68,6 +67,7 @@ func TestAssetStatContracts(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 8, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -75,6 +75,7 @@ func TestAssetStatContracts(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "90", }, Amount: "1", NumAccounts: 2, @@ -87,14 +88,10 @@ func TestAssetStatContracts(t *testing.T) { } tt.Assert.NoError(q.InsertAssetStats(tt.Ctx, assetStats, 1)) - count, err = q.CountContractIDs(tt.Ctx) - tt.Assert.NoError(err) - tt.Assert.Equal(2, count) - contractID[0] = 0 for i := 0; i < 2; i++ { var assetStat ExpAssetStat - assetStat, err = q.GetAssetStatByContract(tt.Ctx, contractID) + assetStat, err := q.GetAssetStatByContract(tt.Ctx, contractID) tt.Assert.NoError(err) tt.Assert.True(assetStat.Equals(assetStats[i])) contractID[0]++ @@ -162,6 +159,7 @@ func TestInsertAssetStats(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -169,6 +167,7 @@ func TestInsertAssetStats(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -181,6 +180,7 @@ func TestInsertAssetStats(t *testing.T) { Authorized: 1, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "23", @@ -188,6 +188,7 @@ func TestInsertAssetStats(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "0", }, Amount: "23", NumAccounts: 1, @@ -217,6 +218,7 @@ func TestInsertAssetStat(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -224,6 +226,7 @@ func TestInsertAssetStat(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -236,6 +239,7 @@ func TestInsertAssetStat(t *testing.T) { Authorized: 1, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "23", @@ -243,6 +247,7 @@ func TestInsertAssetStat(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "0", }, Amount: "23", NumAccounts: 1, @@ -274,6 +279,7 @@ func TestInsertAssetStatAlreadyExistsError(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -281,6 +287,7 @@ func TestInsertAssetStatAlreadyExistsError(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -321,6 +328,7 @@ func TestUpdateAssetStatDoesNotExistsError(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -328,6 +336,7 @@ func TestUpdateAssetStatDoesNotExistsError(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -356,6 +365,7 @@ func TestUpdateStat(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -363,6 +373,7 @@ func TestUpdateStat(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -377,7 +388,10 @@ func TestUpdateStat(t *testing.T) { tt.Assert.Equal(got, assetStat) assetStat.NumAccounts = 50 + assetStat.Accounts.Contracts = 4 assetStat.Amount = "23" + assetStat.Balances.Contracts = "56" + assetStat.SetContractID([32]byte{23}) numChanged, err = q.UpdateAssetStat(tt.Ctx, assetStat) tt.Assert.Nil(err) @@ -385,7 +399,7 @@ func TestUpdateStat(t *testing.T) { got, err = q.GetAssetStat(tt.Ctx, assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) tt.Assert.NoError(err) - tt.Assert.Equal(got, assetStat) + tt.Assert.True(got.Equals(assetStat)) } func TestGetAssetStatDoesNotExist(t *testing.T) { @@ -402,6 +416,7 @@ func TestGetAssetStatDoesNotExist(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -409,6 +424,7 @@ func TestGetAssetStatDoesNotExist(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -433,6 +449,7 @@ func TestRemoveAssetStat(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -440,6 +457,7 @@ func TestRemoveAssetStat(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", + Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -569,6 +587,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -576,6 +595,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { Unauthorized: "3", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -588,6 +608,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { Authorized: 1, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "23", @@ -595,6 +616,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { Unauthorized: "3", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "23", NumAccounts: 1, @@ -607,6 +629,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -614,6 +637,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { Unauthorized: "3", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -626,6 +650,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { Authorized: 3, AuthorizedToMaintainLiabilities: 2, Unauthorized: 4, + Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "111", @@ -633,6 +658,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { Unauthorized: "3", ClaimableBalances: "1", LiquidityPools: "2", + Contracts: "0", }, Amount: "111", NumAccounts: 3, diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index 774c4dec83..a066836e63 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -373,6 +373,7 @@ type ExpAssetStatAccounts struct { AuthorizedToMaintainLiabilities int32 `json:"authorized_to_maintain_liabilities"` ClaimableBalances int32 `json:"claimable_balances"` LiquidityPools int32 `json:"liquidity_pools"` + Contracts int32 `json:"contracts"` Unauthorized int32 `json:"unauthorized"` } @@ -429,6 +430,7 @@ func (a ExpAssetStatAccounts) Add(b ExpAssetStatAccounts) ExpAssetStatAccounts { ClaimableBalances: a.ClaimableBalances + b.ClaimableBalances, LiquidityPools: a.LiquidityPools + b.LiquidityPools, Unauthorized: a.Unauthorized + b.Unauthorized, + Contracts: a.Contracts + b.Contracts, } } @@ -443,9 +445,21 @@ type ExpAssetStatBalances struct { AuthorizedToMaintainLiabilities string `json:"authorized_to_maintain_liabilities"` ClaimableBalances string `json:"claimable_balances"` LiquidityPools string `json:"liquidity_pools"` + Contracts string `json:"contracts"` Unauthorized string `json:"unauthorized"` } +func (e ExpAssetStatBalances) IsZero() bool { + return e == ExpAssetStatBalances{ + Authorized: "0", + AuthorizedToMaintainLiabilities: "0", + ClaimableBalances: "0", + LiquidityPools: "0", + Contracts: "0", + Unauthorized: "0", + } +} + func (e ExpAssetStatBalances) Value() (driver.Value, error) { return json.Marshal(e) } @@ -477,6 +491,9 @@ func (e *ExpAssetStatBalances) Scan(src interface{}) error { if e.Unauthorized == "" { e.Unauthorized = "0" } + if e.Contracts == "" { + e.Contracts = "0" + } return nil } @@ -492,7 +509,6 @@ type QAssetStats interface { RemoveAssetStat(ctx context.Context, assetType xdr.AssetType, assetCode, assetIssuer string) (int64, error) GetAssetStats(ctx context.Context, assetCode, assetIssuer string, page db2.PageQuery) ([]ExpAssetStat, error) CountTrustLines(ctx context.Context) (int, error) - CountContractIDs(ctx context.Context) (int, error) } type QCreateAccountsHistory interface { diff --git a/services/horizon/internal/ingest/processors/asset_stats_processor.go b/services/horizon/internal/ingest/processors/asset_stats_processor.go index ff2df8fe86..32c9733b1c 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_processor.go +++ b/services/horizon/internal/ingest/processors/asset_stats_processor.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/hex" + "math/big" "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" @@ -121,7 +122,7 @@ func (p *AssetStatsProcessor) Commit(ctx context.Context) error { } } - assetStatsDeltas, contractToAsset := p.assetStatSet.All() + assetStatsDeltas, contractToAsset, contractAssetStats := p.assetStatSet.All() for _, delta := range assetStatsDeltas { var rowsAffected int64 var stat history.ExpAssetStat @@ -133,6 +134,12 @@ func (p *AssetStatsProcessor) Commit(ctx context.Context) error { return errors.Wrap(err, "cannot compute contract id for asset") } + if contractAssetStat, ok := contractAssetStats[contractID]; ok { + delta.Balances.Contracts = contractAssetStat.balance.String() + delta.Accounts.Contracts = contractAssetStat.numHolders + delete(contractAssetStats, contractID) + } + stat, err = p.assetStatsQ.GetAssetStat(ctx, delta.AssetType, delta.AssetCode, @@ -279,12 +286,19 @@ func (p *AssetStatsProcessor) Commit(ctx context.Context) error { } } - return p.updateContractIDs(ctx, contractToAsset) + if err := p.updateContractIDs(ctx, contractToAsset, contractAssetStats); err != nil { + return err + } + return p.updateContractAssetStats(ctx, contractAssetStats) } -func (p *AssetStatsProcessor) updateContractIDs(ctx context.Context, contractToAsset map[[32]byte]*xdr.Asset) error { +func (p *AssetStatsProcessor) updateContractIDs( + ctx context.Context, + contractToAsset map[[32]byte]*xdr.Asset, + contractAssetStats map[[32]byte]contractAssetStatValue, +) error { for contractID, asset := range contractToAsset { - if err := p.updateContractID(ctx, contractID, asset); err != nil { + if err := p.updateContractID(ctx, contractAssetStats, contractID, asset); err != nil { return err } } @@ -292,7 +306,12 @@ func (p *AssetStatsProcessor) updateContractIDs(ctx context.Context, contractToA } // updateContractID will update the asset stat row for the corresponding asset to either add or remove the given contract id -func (p *AssetStatsProcessor) updateContractID(ctx context.Context, contractID [32]byte, asset *xdr.Asset) error { +func (p *AssetStatsProcessor) updateContractID( + ctx context.Context, + contractAssetStats map[[32]byte]contractAssetStatValue, + contractID [32]byte, + asset *xdr.Asset, +) error { var rowsAffected int64 // asset is nil so we need to set the contract_id column to NULL if asset == nil { @@ -304,10 +323,20 @@ func (p *AssetStatsProcessor) updateContractID(ctx context.Context, contractID [ )) } if err != nil { - return errors.Wrap(err, "could not find asset stat by contract id") + return errors.Wrap(err, "error querying asset by contract id") + } + + if err = p.maybeAddContractAssetStat(contractAssetStats, contractID, &stat); err != nil { + return errors.Wrapf(err, "could not update asset stat with contract id %v with contract delta", contractID) } if stat.Accounts.IsZero() { + if !stat.Balances.IsZero() { + return ingest.NewStateError(errors.Errorf( + "asset stat has 0 holders but non zero balance: %s", + hex.EncodeToString(contractID[:]), + )) + } // the asset stat is empty so we can remove the row entirely rowsAffected, err = p.assetStatsQ.RemoveAssetStat(ctx, stat.AssetType, @@ -317,6 +346,11 @@ func (p *AssetStatsProcessor) updateContractID(ctx context.Context, contractID [ if err != nil { return errors.Wrap(err, "could not remove asset stat") } + } else if stat.Accounts.Contracts != 0 || stat.Balances.Contracts != "0" { + return ingest.NewStateError(errors.Errorf( + "asset stat has contract holders but is attempting to remove contract id: %s", + hex.EncodeToString(contractID[:]), + )) } else { // update the row to set the contract_id column to NULL stat.ContractID = nil @@ -342,12 +376,16 @@ func (p *AssetStatsProcessor) updateContractID(ctx context.Context, contractID [ NumAccounts: 0, } row.SetContractID(contractID) + if err = p.maybeAddContractAssetStat(contractAssetStats, contractID, &row); err != nil { + return errors.Wrapf(err, "could not update asset stat with contract id %v with contract delta", contractID) + } + rowsAffected, err = p.assetStatsQ.InsertAssetStat(ctx, row) if err != nil { return errors.Wrap(err, "could not insert asset stat") } } else if err != nil { - return errors.Wrap(err, "could not find asset stat by contract id") + return errors.Wrap(err, "error querying asset by asset code and issuer") } else if dbContractID, ok := stat.GetContractID(); ok { // the asset stat already has a column_id set which is unexpected (the column should be NULL) return ingest.NewStateError(errors.Errorf( @@ -359,6 +397,10 @@ func (p *AssetStatsProcessor) updateContractID(ctx context.Context, contractID [ } else { // update the column_id column stat.SetContractID(contractID) + if err = p.maybeAddContractAssetStat(contractAssetStats, contractID, &stat); err != nil { + return errors.Wrapf(err, "could not update asset stat with contract id %v with contract delta", contractID) + } + rowsAffected, err = p.assetStatsQ.UpdateAssetStat(ctx, stat) if err != nil { return errors.Wrap(err, "could not update asset stat") @@ -376,3 +418,69 @@ func (p *AssetStatsProcessor) updateContractID(ctx context.Context, contractID [ } return nil } + +func (p *AssetStatsProcessor) addContractAssetStat(contractAssetStat contractAssetStatValue, stat *history.ExpAssetStat) error { + stat.Accounts.Contracts += contractAssetStat.numHolders + contracts, ok := new(big.Int).SetString(stat.Balances.Contracts, 10) + if !ok { + return errors.New("Error parsing: " + stat.Balances.Contracts) + } + stat.Balances.Contracts = (new(big.Int).Add(contracts, contractAssetStat.balance)).String() + return nil +} + +func (p *AssetStatsProcessor) maybeAddContractAssetStat(contractAssetStats map[[32]byte]contractAssetStatValue, contractID [32]byte, stat *history.ExpAssetStat) error { + if contractAssetStat, ok := contractAssetStats[contractID]; ok { + if err := p.addContractAssetStat(contractAssetStat, stat); err != nil { + return err + } + delete(contractAssetStats, contractID) + } + return nil +} + +func (p *AssetStatsProcessor) updateContractAssetStats( + ctx context.Context, + contractAssetStats map[[32]byte]contractAssetStatValue, +) error { + for contractID, contractAssetStat := range contractAssetStats { + if err := p.updateContractAssetStat(ctx, contractID, contractAssetStat); err != nil { + return err + } + } + return nil +} + +// updateContractAssetStat will look up an asset stat by contract id and, if it exists, +// it will adjust the contract balance and holders based on contractAssetStatValue +func (p *AssetStatsProcessor) updateContractAssetStat( + ctx context.Context, + contractID [32]byte, + contractAssetStat contractAssetStatValue, +) error { + stat, err := p.assetStatsQ.GetAssetStatByContract(ctx, contractID) + if err == sql.ErrNoRows { + return nil + } else if err != nil { + return errors.Wrap(err, "error querying asset by contract id") + } + if err = p.addContractAssetStat(contractAssetStat, &stat); err != nil { + return errors.Wrapf(err, "could not update asset stat with contract id %v with contract delta", contractID) + } + + var rowsAffected int64 + rowsAffected, err = p.assetStatsQ.UpdateAssetStat(ctx, stat) + if err != nil { + return errors.Wrap(err, "could not update asset stat") + } + + if rowsAffected != 1 { + // assert that we have updated exactly one row + return ingest.NewStateError(errors.Errorf( + "%d rows affected (expected exactly 1) when adjusting asset stat for asset: %s", + rowsAffected, + stat.AssetCode+":"+stat.AssetIssuer, + )) + } + return nil +} diff --git a/services/horizon/internal/ingest/processors/asset_stats_processor_test.go b/services/horizon/internal/ingest/processors/asset_stats_processor_test.go index 1f962a7015..26fc0f6fdd 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_processor_test.go +++ b/services/horizon/internal/ingest/processors/asset_stats_processor_test.go @@ -70,6 +70,7 @@ func (s *AssetStatsProcessorTestSuiteState) TestCreateTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 1, @@ -137,6 +138,7 @@ func (s *AssetStatsProcessorTestSuiteState) TestCreateTrustLineWithClawback() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 1, @@ -175,6 +177,7 @@ func (s *AssetStatsProcessorTestSuiteState) TestCreateTrustLineUnauthorized() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -316,6 +319,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertClaimableBalance() { Unauthorized: "0", ClaimableBalances: "24", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -339,6 +343,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertClaimableBalance() { Unauthorized: "0", ClaimableBalances: "46", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -472,6 +477,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "10", NumAccounts: 1, @@ -495,6 +501,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertTrustLine() { Unauthorized: "10", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -573,6 +580,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 1, @@ -599,6 +607,227 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", + }, + Amount: "0", + NumAccounts: 0, + } + usdAssetStat.SetContractID(usdID) + s.mockQ.On("InsertAssetStat", s.ctx, mock.MatchedBy(func(assetStat history.ExpAssetStat) bool { + return usdAssetStat.Equals(assetStat) + })).Return(int64(1), nil).Once() + + s.Assert().NoError(s.processor.Commit(s.ctx)) +} + +func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractBalance() { + lastModifiedLedgerSeq := xdr.Uint32(1234) + usdID, err := xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()).ContractID("") + s.Assert().NoError(err) + + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: BalanceToContractData(usdID, [32]byte{1}, 200), + }, + })) + + usdAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "USD", + Accounts: history.ExpAssetStatAccounts{ + Contracts: 1, + }, + Balances: history.ExpAssetStatBalances{ + Authorized: "0", + AuthorizedToMaintainLiabilities: "0", + Unauthorized: "0", + ClaimableBalances: "0", + LiquidityPools: "0", + Contracts: "150", + }, + Amount: "0", + NumAccounts: 0, + } + usdAssetStat.SetContractID(usdID) + s.mockQ.On("GetAssetStatByContract", s.ctx, usdID). + Return(usdAssetStat, nil).Once() + + usdAssetStat.Accounts.Contracts++ + usdAssetStat.Balances.Contracts = "350" + s.mockQ.On("UpdateAssetStat", s.ctx, mock.MatchedBy(func(assetStat history.ExpAssetStat) bool { + return usdAssetStat.Equals(assetStat) + })).Return(int64(1), nil).Once() + + s.Assert().NoError(s.processor.Commit(s.ctx)) +} + +func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractBalance() { + lastModifiedLedgerSeq := xdr.Uint32(1234) + usdID, err := xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()).ContractID("") + s.Assert().NoError(err) + + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: BalanceToContractData(usdID, [32]byte{1}, 200), + }, + })) + + usdAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "USD", + Accounts: history.ExpAssetStatAccounts{ + Contracts: 1, + }, + Balances: history.ExpAssetStatBalances{ + Authorized: "0", + AuthorizedToMaintainLiabilities: "0", + Unauthorized: "0", + ClaimableBalances: "0", + LiquidityPools: "0", + Contracts: "200", + }, + Amount: "0", + NumAccounts: 0, + } + usdAssetStat.SetContractID(usdID) + s.mockQ.On("GetAssetStatByContract", s.ctx, usdID). + Return(usdAssetStat, nil).Once() + + usdAssetStat.Accounts.Contracts = 0 + usdAssetStat.Balances.Contracts = "0" + s.mockQ.On("UpdateAssetStat", s.ctx, mock.MatchedBy(func(assetStat history.ExpAssetStat) bool { + return usdAssetStat.Equals(assetStat) + })).Return(int64(1), nil).Once() + + s.Assert().NoError(s.processor.Commit(s.ctx)) +} + +func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractIDWithBalance() { + // add trust line + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ToTrustLineAsset(), + Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + eurID, err := trustLine.Asset.ToAsset().ContractID("") + s.Assert().NoError(err) + eurContractData, err := AssetToContractData(false, "EUR", trustLineIssuer.Address(), eurID) + s.Assert().NoError(err) + + usdID, err := xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()).ContractID("") + s.Assert().NoError(err) + usdContractData, err := AssetToContractData(false, "USD", trustLineIssuer.Address(), usdID) + s.Assert().NoError(err) + + lastModifiedLedgerSeq := xdr.Uint32(1234) + err = s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTrustline, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + }, + }) + s.Assert().NoError(err) + + err = s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: eurContractData, + }, + }) + s.Assert().NoError(err) + + err = s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: usdContractData, + }, + }) + s.Assert().NoError(err) + + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: BalanceToContractData(usdID, [32]byte{1}, 150), + }, + })) + + btcID := [32]byte{1, 2, 3} + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: BalanceToContractData(btcID, [32]byte{1}, 20), + }, + })) + + s.mockQ.On("GetAssetStat", s.ctx, + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() + eurAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Accounts: history.ExpAssetStatAccounts{ + Authorized: 1, + }, + Balances: history.ExpAssetStatBalances{ + Authorized: "0", + AuthorizedToMaintainLiabilities: "0", + Unauthorized: "0", + ClaimableBalances: "0", + LiquidityPools: "0", + Contracts: "0", + }, + Amount: "0", + NumAccounts: 1, + } + eurAssetStat.SetContractID(eurID) + s.mockQ.On("InsertAssetStat", s.ctx, mock.MatchedBy(func(assetStat history.ExpAssetStat) bool { + return eurAssetStat.Equals(assetStat) + })).Return(int64(1), nil).Once() + + s.mockQ.On("GetAssetStat", s.ctx, + xdr.AssetTypeAssetTypeCreditAlphanum4, + "USD", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() + + s.mockQ.On("GetAssetStatByContract", s.ctx, btcID). + Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() + + usdAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "USD", + Accounts: history.ExpAssetStatAccounts{ + Contracts: 1, + }, + Balances: history.ExpAssetStatBalances{ + Authorized: "0", + AuthorizedToMaintainLiabilities: "0", + Unauthorized: "0", + ClaimableBalances: "0", + LiquidityPools: "0", + Contracts: "150", }, Amount: "0", NumAccounts: 0, @@ -705,6 +934,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertClaimableBalanceAndTrustl Unauthorized: "0", ClaimableBalances: "12", LiquidityPools: "100", + Contracts: "0", }, Amount: "9", NumAccounts: 1, @@ -745,6 +975,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -761,6 +992,80 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", + }, + Amount: "100", + NumAccounts: 1, + } + eurAssetStat.SetContractID(eurID) + s.mockQ.On("UpdateAssetStat", s.ctx, mock.MatchedBy(func(assetStat history.ExpAssetStat) bool { + return eurAssetStat.Equals(assetStat) + })).Return(int64(1), nil).Once() + + s.Assert().NoError(s.processor.Commit(s.ctx)) +} + +func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractIDWithBalance() { + lastModifiedLedgerSeq := xdr.Uint32(1234) + + eurID, err := xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ContractID("") + s.Assert().NoError(err) + eurContractData, err := AssetToContractData(false, "EUR", trustLineIssuer.Address(), eurID) + s.Assert().NoError(err) + + err = s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: eurContractData, + }, + }) + s.Assert().NoError(err) + + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: BalanceToContractData(eurID, [32]byte{1}, 150), + }, + })) + + s.mockQ.On("GetAssetStat", s.ctx, + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Accounts: history.ExpAssetStatAccounts{Authorized: 1}, + Balances: history.ExpAssetStatBalances{ + Authorized: "100", + AuthorizedToMaintainLiabilities: "0", + Unauthorized: "0", + ClaimableBalances: "0", + LiquidityPools: "0", + Contracts: "0", + }, + Amount: "100", + NumAccounts: 1, + }, nil).Once() + + eurAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Accounts: history.ExpAssetStatAccounts{ + Authorized: 1, + Contracts: 1, + }, + Balances: history.ExpAssetStatBalances{ + Authorized: "100", + AuthorizedToMaintainLiabilities: "0", + Unauthorized: "0", + ClaimableBalances: "0", + LiquidityPools: "0", + Contracts: "150", }, Amount: "100", NumAccounts: 1, @@ -803,6 +1108,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractIDError() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -882,6 +1188,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustlineAndContractIDErr Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -985,6 +1292,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustlineAndRemoveContrac Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1051,6 +1359,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1066,6 +1375,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "110", NumAccounts: 1, @@ -1191,6 +1501,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "100", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1208,6 +1519,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "10", NumAccounts: 1, @@ -1230,6 +1542,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1247,6 +1560,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "10", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1269,6 +1583,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1286,6 +1601,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1353,6 +1669,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveClaimableBalance() { Unauthorized: "0", ClaimableBalances: "12", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1381,6 +1698,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveClaimableBalance() { Unauthorized: "0", ClaimableBalances: "21", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1396,6 +1714,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveClaimableBalance() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1458,6 +1777,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 1, @@ -1485,6 +1805,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1526,6 +1847,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1602,6 +1924,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustlineAndRemoveContrac Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1624,6 +1947,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustlineAndRemoveContrac Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "110", NumAccounts: 1, @@ -1663,6 +1987,69 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDFromZeroRow() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", + }, + Amount: "0", + NumAccounts: 0, + } + eurAssetStat.SetContractID(eurID) + s.mockQ.On("GetAssetStatByContract", s.ctx, eurID). + Return(eurAssetStat, nil).Once() + + s.mockQ.On("RemoveAssetStat", s.ctx, + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(int64(1), nil).Once() + + s.Assert().NoError(s.processor.Commit(s.ctx)) +} + +func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDAndBalanceZeroRow() { + lastModifiedLedgerSeq := xdr.Uint32(1234) + + eurID, err := xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ContractID("") + s.Assert().NoError(err) + eurContractData, err := AssetToContractData(false, "EUR", trustLineIssuer.Address(), eurID) + s.Assert().NoError(err) + + err = s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: eurContractData, + }, + }) + s.Assert().NoError(err) + + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: BalanceToContractData(eurID, [32]byte{1}, 9), + }, + })) + + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: BalanceToContractData(eurID, [32]byte{2}, 1), + }, + })) + + eurAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Accounts: history.ExpAssetStatAccounts{Contracts: 2}, + Balances: history.ExpAssetStatBalances{ + Authorized: "0", + AuthorizedToMaintainLiabilities: "0", + Unauthorized: "0", + ClaimableBalances: "0", + LiquidityPools: "0", + Contracts: "10", }, Amount: "0", NumAccounts: 0, @@ -1728,6 +2115,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDAndRow() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "0", NumAccounts: 1, @@ -1814,6 +2202,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestProcessUpgradeChange() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "10", NumAccounts: 1, diff --git a/services/horizon/internal/ingest/processors/asset_stats_set.go b/services/horizon/internal/ingest/processors/asset_stats_set.go index 092391fe02..337bf3264b 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_set.go +++ b/services/horizon/internal/ingest/processors/asset_stats_set.go @@ -6,7 +6,6 @@ import ( "math/big" "github.com/stellar/go/ingest" - "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" @@ -30,6 +29,7 @@ type assetStatBalances struct { ClaimableBalances *big.Int LiquidityPools *big.Int Unauthorized *big.Int + Contracts *big.Int } func newAssetStatBalance() assetStatBalances { @@ -39,6 +39,7 @@ func newAssetStatBalance() assetStatBalances { ClaimableBalances: big.NewInt(0), LiquidityPools: big.NewInt(0), Unauthorized: big.NewInt(0), + Contracts: big.NewInt(0), } } @@ -73,6 +74,12 @@ func (a *assetStatBalances) Parse(b *history.ExpAssetStatBalances) error { } a.Unauthorized = unauthorized + contracts, ok := new(big.Int).SetString(b.Contracts, 10) + if !ok { + return errors.New("Error parsing: " + b.Contracts) + } + a.Contracts = contracts + return nil } @@ -83,6 +90,7 @@ func (a assetStatBalances) Add(b assetStatBalances) assetStatBalances { ClaimableBalances: big.NewInt(0).Add(a.ClaimableBalances, b.ClaimableBalances), LiquidityPools: big.NewInt(0).Add(a.LiquidityPools, b.LiquidityPools), Unauthorized: big.NewInt(0).Add(a.Unauthorized, b.Unauthorized), + Contracts: big.NewInt(0).Add(a.Contracts, b.Contracts), } } @@ -91,7 +99,8 @@ func (a assetStatBalances) IsZero() bool { a.AuthorizedToMaintainLiabilities.Cmp(big.NewInt(0)) == 0 && a.ClaimableBalances.Cmp(big.NewInt(0)) == 0 && a.LiquidityPools.Cmp(big.NewInt(0)) == 0 && - a.Unauthorized.Cmp(big.NewInt(0)) == 0 + a.Unauthorized.Cmp(big.NewInt(0)) == 0 && + a.Contracts.Cmp(big.NewInt(0)) == 0 } func (a assetStatBalances) ConvertToHistoryObject() history.ExpAssetStatBalances { @@ -101,6 +110,7 @@ func (a assetStatBalances) ConvertToHistoryObject() history.ExpAssetStatBalances ClaimableBalances: a.ClaimableBalances.String(), LiquidityPools: a.LiquidityPools.String(), Unauthorized: a.Unauthorized.String(), + Contracts: a.Contracts.String(), } } @@ -117,21 +127,28 @@ func (value assetStatValue) ConvertToHistoryObject() history.ExpAssetStat { } } +type contractAssetStatValue struct { + balance *big.Int + numHolders int32 +} + // AssetStatSet represents a collection of asset stats and a mapping // of Soroban contract IDs to classic assets (which is unique to each // network). type AssetStatSet struct { - classicAssetStats map[assetStatKey]*assetStatValue - contractToAsset map[[32]byte]*xdr.Asset - networkPassphrase string + classicAssetStats map[assetStatKey]*assetStatValue + contractToAsset map[[32]byte]*xdr.Asset + contractAssetStats map[[32]byte]contractAssetStatValue + networkPassphrase string } // NewAssetStatSet constructs a new AssetStatSet instance func NewAssetStatSet(networkPassphrase string) AssetStatSet { return AssetStatSet{ - classicAssetStats: map[assetStatKey]*assetStatValue{}, - contractToAsset: map[[32]byte]*xdr.Asset{}, - networkPassphrase: networkPassphrase, + classicAssetStats: map[assetStatKey]*assetStatValue{}, + contractToAsset: map[[32]byte]*xdr.Asset{}, + contractAssetStats: map[[32]byte]contractAssetStatValue{}, + networkPassphrase: networkPassphrase, } } @@ -349,10 +366,17 @@ func (s AssetStatSet) AddClaimableBalance(change ingest.Change) error { // AddContractData updates the set to account for how a given contract data entry has changed. // change must be a xdr.LedgerEntryTypeContractData type. func (s AssetStatSet) AddContractData(change ingest.Change) error { + if err := s.ingestAssetContractMetadata(change); err != nil { + return err + } + s.ingestAssetContractBalance(change) + return nil +} + +func (s AssetStatSet) ingestAssetContractMetadata(change ingest.Change) error { if change.Pre != nil { asset := AssetFromContractData(*change.Pre, s.networkPassphrase) - // we don't support asset stats for lumens - if asset == nil || asset.Type == xdr.AssetTypeAssetTypeNative { + if asset == nil { return nil } contractID := change.Pre.Data.MustContractData().ContractId @@ -369,8 +393,7 @@ func (s AssetStatSet) AddContractData(change ingest.Change) error { } } else if change.Post != nil { asset := AssetFromContractData(*change.Post, s.networkPassphrase) - // we don't support asset stats for lumens - if asset == nil || asset.Type == xdr.AssetTypeAssetTypeNative { + if asset == nil { return nil } contractID := change.Post.Data.MustContractData().ContractId @@ -379,9 +402,93 @@ func (s AssetStatSet) AddContractData(change ingest.Change) error { return nil } +func (s AssetStatSet) ingestAssetContractBalance(change ingest.Change) { + if change.Pre != nil { + contractID := change.Pre.Data.MustContractData().ContractId + holder, amt, ok := ContractBalanceFromContractData(*change.Pre, s.networkPassphrase) + if !ok { + return + } + stats, ok := s.contractAssetStats[contractID] + if !ok { + stats = contractAssetStatValue{ + balance: big.NewInt(0), + numHolders: 0, + } + } + + if change.Post == nil { + // the balance was removed so we need to deduct from + // contract holders and contract balance amount + stats.balance = new(big.Int).Sub(stats.balance, amt) + // only decrement holders if the removed balance + // contained a positive amount of the asset. + if amt.Cmp(big.NewInt(0)) > 0 { + stats.numHolders-- + } + s.maybeAddContractAssetStat(contractID, stats) + return + } + // if the updated ledger entry is not in the expected format then this + // cannot be emitted by the stellar asset contract, so ignore it + postHolder, postAmt, postOk := ContractBalanceFromContractData(*change.Post, s.networkPassphrase) + if !postOk || postHolder != holder { + return + } + + delta := new(big.Int).Sub(postAmt, amt) + stats.balance.Add(stats.balance, delta) + if postAmt.Cmp(big.NewInt(0)) == 0 && amt.Cmp(big.NewInt(0)) > 0 { + // if the pre amount is equal to the post amount it means the balance was wiped out so + // we can decrement the number of contract holders + stats.numHolders-- + } else if amt.Cmp(big.NewInt(0)) == 0 && postAmt.Cmp(big.NewInt(0)) > 0 { + // if the pre amount was zero and the post amount is positive the number of + // contract holders increased + stats.numHolders++ + } + s.maybeAddContractAssetStat(contractID, stats) + return + } + // in this case there was no balance before the change + contractID := change.Post.Data.MustContractData().ContractId + _, amt, ok := ContractBalanceFromContractData(*change.Post, s.networkPassphrase) + if !ok { + return + } + + // ignore zero balance amounts + if amt.Cmp(big.NewInt(0)) == 0 { + return + } + + // increase the number of contract holders because previously + // there was no balance + stats, ok := s.contractAssetStats[contractID] + if !ok { + stats = contractAssetStatValue{ + balance: amt, + numHolders: 1, + } + } else { + stats.balance = new(big.Int).Add(stats.balance, amt) + stats.numHolders++ + } + + s.maybeAddContractAssetStat(contractID, stats) +} + +func (s AssetStatSet) maybeAddContractAssetStat(contractID [32]byte, stat contractAssetStatValue) { + if stat.numHolders == 0 && stat.balance.Cmp(big.NewInt(0)) == 0 { + delete(s.contractAssetStats, contractID) + } else { + s.contractAssetStats[contractID] = stat + } +} + // All returns a list of all `history.ExpAssetStat` contained within the set // along with all contract id attribution changes in the set. -func (s AssetStatSet) All() ([]history.ExpAssetStat, map[[32]byte]*xdr.Asset) { +func (s AssetStatSet) All() ([]history.ExpAssetStat, map[[32]byte]*xdr.Asset, map[[32]byte]contractAssetStatValue) { assetStats := make([]history.ExpAssetStat, 0, len(s.classicAssetStats)) for _, value := range s.classicAssetStats { assetStats = append(assetStats, value.ConvertToHistoryObject()) @@ -390,7 +497,11 @@ func (s AssetStatSet) All() ([]history.ExpAssetStat, map[[32]byte]*xdr.Asset) { for key, val := range s.contractToAsset { contractToAsset[key] = val } - return assetStats, contractToAsset + contractAssetStats := make(map[[32]byte]contractAssetStatValue, len(s.contractAssetStats)) + for key, val := range s.contractAssetStats { + contractAssetStats[key] = val + } + return assetStats, contractToAsset, contractAssetStats } // AllFromSnapshot returns a list of all `history.ExpAssetStat` contained within the set. @@ -399,7 +510,7 @@ func (s AssetStatSet) All() ([]history.ExpAssetStat, map[[32]byte]*xdr.Asset) { // the ledger without any missing entries (e.g. history archives). func (s AssetStatSet) AllFromSnapshot() ([]history.ExpAssetStat, error) { // merge assetStatsDeltas and contractToAsset into one list of history.ExpAssetStat. - assetStatsDeltas, contractToAsset := s.All() + assetStatsDeltas, contractToAsset, contractAssetStats := s.All() // modify the asset stat row to update the contract_id column whenever we encounter a // contract data ledger entry with the Stellar asset metadata. @@ -417,9 +528,14 @@ func (s AssetStatSet) AllFromSnapshot() ([]history.ExpAssetStat, error) { )) } else if ok { assetStatDelta.SetContractID(contractID) - assetStatsDeltas[i] = assetStatDelta delete(contractToAsset, contractID) } + + if stats, ok := contractAssetStats[contractID]; ok { + assetStatDelta.Accounts.Contracts = stats.numHolders + assetStatDelta.Balances.Contracts = stats.balance.String() + } + assetStatsDeltas[i] = assetStatDelta } // There is also a corner case where a Stellar Asset contract is initialized before there exists any @@ -445,8 +561,15 @@ func (s AssetStatSet) AllFromSnapshot() ([]history.ExpAssetStat, error) { Amount: "0", NumAccounts: 0, } + if stats, ok := contractAssetStats[contractID]; ok { + row.Accounts.Contracts = stats.numHolders + row.Balances.Contracts = stats.balance.String() + } row.SetContractID(contractID) assetStatsDeltas = append(assetStatsDeltas, row) } + // all balances remaining in contractAssetStats do not belong to + // stellar asset contracts (because all stellar asset contracts must + // be in contractToAsset) so we can ignore them return assetStatsDeltas, nil } diff --git a/services/horizon/internal/ingest/processors/asset_stats_set_test.go b/services/horizon/internal/ingest/processors/asset_stats_set_test.go index cbaf442aa2..6352ad2bef 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_set_test.go +++ b/services/horizon/internal/ingest/processors/asset_stats_set_test.go @@ -3,6 +3,7 @@ package processors import ( "github.com/stellar/go/keypair" "math" + "math/big" "sort" "testing" @@ -15,8 +16,9 @@ import ( func TestEmptyAssetStatSet(t *testing.T) { set := NewAssetStatSet("") - all, m := set.All() + all, m, cs := set.All() assert.Empty(t, all) + assert.Empty(t, cs) assert.Empty(t, m) all, err := set.AllFromSnapshot() @@ -25,8 +27,9 @@ func TestEmptyAssetStatSet(t *testing.T) { } func assertAllEquals(t *testing.T, set AssetStatSet, expected []history.ExpAssetStat) { - all, m := set.All() + all, m, cs := set.All() assert.Empty(t, m) + assert.Empty(t, cs) assertAssetStatsAreEqual(t, all, expected) } @@ -57,6 +60,9 @@ func TestAddContractData(t *testing.T) { etherAsset := xdr.MustNewCreditAsset("ETHER", etherIssuer) etherID, err := etherAsset.ContractID("passphrase") assert.NoError(t, err) + uniAsset := xdr.MustNewCreditAsset("UNI", etherIssuer) + uniID, err := uniAsset.ContractID("passphrase") + assert.NoError(t, err) set := NewAssetStatSet("passphrase") @@ -70,6 +76,22 @@ func TestAddContractData(t *testing.T) { }) assert.NoError(t, err) + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(xlmID, [32]byte{}, 100), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(uniID, [32]byte{}, 0), + }, + }) + assert.NoError(t, err) + usdcContractData, err := AssetToContractData(false, "USDC", usdcIssuer, usdcID) assert.NoError(t, err) err = set.AddContractData(ingest.Change{ @@ -90,6 +112,43 @@ func TestAddContractData(t *testing.T) { }) assert.NoError(t, err) + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 50), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{1}, 150), + }, + }) + assert.NoError(t, err) + + // negative balances will be ignored + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: balanceToContractData(etherID, [32]byte{1}, xdr.Int128Parts{Hi: 1 << 63, Lo: 0}), + }, + }) + assert.NoError(t, err) + + btcAsset := xdr.MustNewCreditAsset("BTC", etherIssuer) + btcID, err := btcAsset.ContractID("passphrase") + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 300), + }, + }) + assert.NoError(t, err) + assert.NoError( t, set.AddTrustline(trustlineChange(nil, &xdr.TrustLineEntry{ @@ -100,7 +159,7 @@ func TestAddContractData(t *testing.T) { })), ) - all, m := set.All() + all, m, cs := set.All() assert.Len(t, all, 1) etherAssetStat := history.ExpAssetStat{ AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, @@ -115,15 +174,20 @@ func TestAddContractData(t *testing.T) { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "1", NumAccounts: 1, } assert.True(t, all[0].Equals(etherAssetStat)) - assert.Len(t, m, 2) assert.True(t, m[usdcID].Equals(usdcAsset)) assert.True(t, m[etherID].Equals(etherAsset)) + assert.Len(t, cs, 2) + assert.Equal(t, cs[etherID].numHolders, int32(2)) + assert.Zero(t, cs[etherID].balance.Cmp(big.NewInt(200))) + assert.Equal(t, cs[btcID].numHolders, int32(1)) + assert.Zero(t, cs[btcID].balance.Cmp(big.NewInt(300))) usdcAssetStat := history.ExpAssetStat{ AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, @@ -137,6 +201,8 @@ func TestAddContractData(t *testing.T) { } etherAssetStat.SetContractID(etherID) + etherAssetStat.Balances.Contracts = "200" + etherAssetStat.Accounts.Contracts = 2 usdcAssetStat.SetContractID(usdcID) assertAllFromSnapshotEquals(t, set, []history.ExpAssetStat{ @@ -145,6 +211,177 @@ func TestAddContractData(t *testing.T) { }) } +func TestUpdateContractBalance(t *testing.T) { + usdcIssuer := keypair.MustRandom().Address() + usdcAsset := xdr.MustNewCreditAsset("USDC", usdcIssuer) + usdcID, err := usdcAsset.ContractID("passphrase") + assert.NoError(t, err) + etherIssuer := keypair.MustRandom().Address() + etherAsset := xdr.MustNewCreditAsset("ETHER", etherIssuer) + etherID, err := etherAsset.ContractID("passphrase") + assert.NoError(t, err) + btcAsset := xdr.MustNewCreditAsset("BTC", etherIssuer) + btcID, err := btcAsset.ContractID("passphrase") + assert.NoError(t, err) + uniAsset := xdr.MustNewCreditAsset("UNI", etherIssuer) + uniID, err := uniAsset.ContractID("passphrase") + assert.NoError(t, err) + + set := NewAssetStatSet("passphrase") + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{}, 50), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{}, 100), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{2}, 30), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{2}, 100), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{4}, 0), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{4}, 100), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 200), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 50), + }, + }) + assert.NoError(t, err) + + // negative balances will be ignored + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 200), + }, + Post: &xdr.LedgerEntry{ + Data: balanceToContractData(etherID, [32]byte{1}, xdr.Int128Parts{Hi: 1 << 63, Lo: 0}), + }, + }) + assert.NoError(t, err) + + // negative balances will be ignored + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: balanceToContractData(etherID, [32]byte{1}, xdr.Int128Parts{Hi: 1 << 63, Lo: 0}), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 200), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 300), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 300), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 0), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 0), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 0), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 0), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(uniID, [32]byte{2}, 300), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(uniID, [32]byte{3}, 100), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(uniID, [32]byte{3}, 0), + }, + }) + assert.NoError(t, err) + + err = set.AddContractData(ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(uniID, [32]byte{4}, 100), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(uniID, [32]byte{4}, 50), + }, + }) + assert.NoError(t, err) + + all, m, cs := set.All() + assert.Empty(t, all) + assert.Empty(t, m) + + assert.Len(t, cs, 3) + assert.Equal(t, cs[usdcID].numHolders, int32(1)) + assert.Zero(t, cs[usdcID].balance.Cmp(big.NewInt(220))) + assert.Equal(t, cs[etherID].numHolders, int32(0)) + assert.Zero(t, cs[etherID].balance.Cmp(big.NewInt(-150))) + assert.Equal(t, cs[uniID].numHolders, int32(-2)) + assert.Zero(t, cs[uniID].balance.Cmp(big.NewInt(-450))) + + all, err = set.AllFromSnapshot() + assert.NoError(t, err) + assert.Empty(t, all) +} + func TestRemoveContractData(t *testing.T) { eurID, err := xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ContractID("passphrase") assert.NoError(t, err) @@ -160,8 +397,9 @@ func TestRemoveContractData(t *testing.T) { }) assert.NoError(t, err) - all, m := set.All() + all, m, cs := set.All() assert.Empty(t, all) + assert.Empty(t, cs) assert.Len(t, m, 1) asset, ok := m[eurID] assert.True(t, ok) @@ -212,9 +450,10 @@ func TestAddNativeClaimableBalance(t *testing.T) { }, }, )) - all, m := set.All() + all, m, cs := set.All() assert.Empty(t, all) assert.Empty(t, m) + assert.Empty(t, cs) } func trustlineChange(pre, post *xdr.TrustLineEntry) ingest.Change { @@ -251,9 +490,10 @@ func TestAddPoolShareTrustline(t *testing.T) { }, )), ) - all, m := set.All() + all, m, cs := set.All() assert.Empty(t, all) assert.Empty(t, m) + assert.Empty(t, cs) } func TestAddAssetStats(t *testing.T) { @@ -272,6 +512,7 @@ func TestAddAssetStats(t *testing.T) { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "1", NumAccounts: 1, @@ -377,6 +618,7 @@ func TestAddAssetStats(t *testing.T) { Unauthorized: "5", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "3", NumAccounts: 1, @@ -395,6 +637,7 @@ func TestAddAssetStats(t *testing.T) { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "10", NumAccounts: 1, @@ -415,13 +658,12 @@ func TestOverflowAssetStatSet(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } - all, m := set.All() + all, m, cs := set.All() if len(all) != 1 { t.Fatalf("expected list of 1 asset stat but got %v", all) } - if len(m) != 0 { - t.Fatalf("expected contract id map to be empty but got %v", m) - } + assert.Empty(t, m) + assert.Empty(t, cs) eurAssetStat := history.ExpAssetStat{ AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, @@ -436,6 +678,7 @@ func TestOverflowAssetStatSet(t *testing.T) { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "9223372036854775807", NumAccounts: 1, @@ -453,13 +696,12 @@ func TestOverflowAssetStatSet(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } - all, m = set.All() + all, m, cs = set.All() if len(all) != 1 { t.Fatalf("expected list of 1 asset stat but got %v", all) } - if len(m) != 0 { - t.Fatalf("expected contract id map to be empty but got %v", m) - } + assert.Empty(t, m) + assert.Empty(t, cs) eurAssetStat = history.ExpAssetStat{ AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, @@ -474,6 +716,7 @@ func TestOverflowAssetStatSet(t *testing.T) { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", + Contracts: "0", }, Amount: "18446744073709551614", NumAccounts: 2, diff --git a/services/horizon/internal/ingest/processors/contract_data.go b/services/horizon/internal/ingest/processors/contract_data.go index e832a80b81..80c4d9c058 100644 --- a/services/horizon/internal/ingest/processors/contract_data.go +++ b/services/horizon/internal/ingest/processors/contract_data.go @@ -1,13 +1,16 @@ package processors import ( + "math/big" + "github.com/stellar/go/strkey" "github.com/stellar/go/xdr" ) var ( - assetMetadataSym = xdr.ScSymbol("Metadata") - assetMetadataObj = &xdr.ScObject{ + balanceMetadataSym = xdr.ScSymbol("Balance") + assetMetadataSym = xdr.ScSymbol("Metadata") + assetMetadataObj = &xdr.ScObject{ Type: xdr.ScObjectTypeScoVec, Vec: &xdr.ScVec{ xdr.ScVal{ @@ -34,39 +37,35 @@ var ( // If the given ledger entry is a verified asset metadata entry AssetFromContractData will // return the corresponding Stellar asset. Otherwise, AssetFromContractData will return nil. func AssetFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) *xdr.Asset { - if ledgerEntry.Data.Type != xdr.LedgerEntryTypeContractData { + contractData, ok := ledgerEntry.Data.GetContractData() + if !ok { return nil } - contractData := ledgerEntry.Data.MustContractData() - if !contractData.Key.Equals(assetMetadataKey) { + + // we don't support asset stats for lumens + nativeAssetContractID, err := xdr.MustNewNativeAsset().ContractID(passphrase) + if err != nil || contractData.ContractId == nativeAssetContractID { return nil } - if contractData.Val.Type != xdr.ScValTypeScvObject { + + if !contractData.Key.Equals(assetMetadataKey) { return nil } - obj := contractData.Val.MustObj() - if obj.Type != xdr.ScObjectTypeScoVec { + obj, ok := contractData.Val.GetObj() + if !ok || obj == nil { return nil } - vec := obj.MustVec() - if len(vec) <= 0 { + + vec, ok := obj.GetVec() + if !ok || len(vec) <= 0 { return nil } - if vec[0].Type != xdr.ScValTypeScvSymbol { + sym, ok := vec[0].GetSym() + if !ok { return nil } - switch vec[0].MustSym() { - case "Native": - asset := xdr.MustNewNativeAsset() - nativeAssetContractID, err := asset.ContractID(passphrase) - if err != nil { - return nil - } - if contractData.ContractId == nativeAssetContractID { - return &asset - } - return nil + switch sym { case "AlphaNum4": case "AlphaNum12": default: @@ -77,48 +76,38 @@ func AssetFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) *xdr. } var assetCode, assetIssuer string - if vec[1].Type != xdr.ScValTypeScvObject { - return nil - } - obj = vec[1].MustObj() - if obj.Type != xdr.ScObjectTypeScoMap { + obj, ok = vec[1].GetObj() + if !ok || obj == nil { return nil } - assetMap := obj.MustMap() - if len(assetMap) != 2 { + assetMap, ok := obj.GetMap() + if !ok || len(assetMap) != 2 { return nil } assetCodeEntry, assetIssuerEntry := assetMap[0], assetMap[1] - if assetCodeEntry.Key.Type != xdr.ScValTypeScvSymbol { + if sym, ok = assetCodeEntry.Key.GetSym(); !ok || sym != "asset_code" { return nil } - if assetCodeEntry.Key.MustSym() != "asset_code" { + if obj, ok = assetCodeEntry.Val.GetObj(); !ok || obj == nil { return nil } - if assetCodeEntry.Val.Type != xdr.ScValTypeScvObject { + bin, ok := obj.GetBin() + if !ok { return nil } - obj = assetCodeEntry.Val.MustObj() - if obj.Type != xdr.ScObjectTypeScoBytes { - return nil - } - assetCode = string(obj.MustBin()) + assetCode = string(bin) - if assetIssuerEntry.Key.Type != xdr.ScValTypeScvSymbol { + if sym, ok = assetIssuerEntry.Key.GetSym(); !ok || sym != "issuer" { return nil } - if assetIssuerEntry.Key.MustSym() != "issuer" { + if obj, ok = assetIssuerEntry.Val.GetObj(); !ok || obj == nil { return nil } - if assetIssuerEntry.Val.Type != xdr.ScValTypeScvObject { + bin, ok = obj.GetBin() + if !ok { return nil } - obj = assetIssuerEntry.Val.MustObj() - if obj.Type != xdr.ScObjectTypeScoBytes { - return nil - } - var err error - assetIssuer, err = strkey.Encode(strkey.VersionByteAccountID, obj.MustBin()) + assetIssuer, err = strkey.Encode(strkey.VersionByteAccountID, bin) if err != nil { return nil } @@ -139,6 +128,83 @@ func AssetFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) *xdr. return &asset } +// ContractBalanceFromContractData takes a ledger entry and verifies if the ledger entry corresponds +// to the balance entry written to contract storage by the Stellar Asset Contract. See: +// https://github.com/stellar/rs-soroban-env/blob/5695440da452837555d8f7f259cc33341fdf07b0/soroban-env-host/src/native_contract/token/storage_types.rs#L11-L24 +func ContractBalanceFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) ([32]byte, *big.Int, bool) { + contractData, ok := ledgerEntry.Data.GetContractData() + if !ok { + return [32]byte{}, nil, false + } + + // we don't support asset stats for lumens + nativeAssetContractID, err := xdr.MustNewNativeAsset().ContractID(passphrase) + if err != nil || contractData.ContractId == nativeAssetContractID { + return [32]byte{}, nil, false + } + + keyObj, ok := contractData.Key.GetObj() + if !ok || keyObj == nil { + return [32]byte{}, nil, false + } + keyEnumVec, ok := keyObj.GetVec() + if !ok || len(keyEnumVec) != 2 || !keyEnumVec[0].Equals( + xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &balanceMetadataSym}, + ) { + return [32]byte{}, nil, false + } + addressObj, ok := keyEnumVec[1].GetObj() + if !ok || addressObj == nil { + return [32]byte{}, nil, false + } + scAddress, ok := addressObj.GetAddress() + if !ok { + return [32]byte{}, nil, false + } + holder, ok := scAddress.GetContractId() + if !ok { + return [32]byte{}, nil, false + } + + obj, ok := contractData.Val.GetObj() + if !ok || obj == nil { + return [32]byte{}, nil, false + } + balanceMap, ok := obj.GetMap() + if !ok || len(balanceMap) != 3 { + return [32]byte{}, nil, false + } + + var keySym xdr.ScSymbol + if keySym, ok = balanceMap[0].Key.GetSym(); !ok || keySym != "amount" { + return [32]byte{}, nil, false + } + if keySym, ok = balanceMap[1].Key.GetSym(); !ok || keySym != "authorized" || + !balanceMap[1].Val.IsBool() { + return [32]byte{}, nil, false + } + if keySym, ok = balanceMap[2].Key.GetSym(); !ok || keySym != "clawback" || + !balanceMap[2].Val.IsBool() { + return [32]byte{}, nil, false + } + amountObj, ok := balanceMap[0].Val.GetObj() + if !ok || amountObj == nil { + return [32]byte{}, nil, false + } + amount, ok := amountObj.GetI128() + if !ok { + return [32]byte{}, nil, false + } + // amount cannot be negative + // https://github.com/stellar/rs-soroban-env/blob/a66f0815ba06a2f5328ac420950690fd1642f887/soroban-env-host/src/native_contract/token/balance.rs#L92-L93 + if int64(amount.Hi) < 0 { + return [32]byte{}, nil, false + } + amt := new(big.Int).Lsh(new(big.Int).SetUint64(uint64(amount.Hi)), 64) + amt.Add(amt, new(big.Int).SetUint64(uint64(amount.Lo))) + return holder, amt, true +} + func metadataObjFromAsset(isNative bool, code, issuer string) (*xdr.ScObject, error) { if isNative { symbol := xdr.ScSymbol("Native") @@ -241,3 +307,92 @@ func AssetToContractData(isNative bool, code, issuer string, contractID [32]byte }, }, nil } + +// BalanceToContractData is the inverse of ContractBalanceFromContractData. It creates a ledger entry +// containing the asset balance of a contract holder written to contract storage by the Stellar Asset Contract. +func BalanceToContractData(assetContractId, holderID [32]byte, amt uint64) xdr.LedgerEntryData { + return balanceToContractData(assetContractId, holderID, xdr.Int128Parts{ + Lo: xdr.Uint64(amt), + Hi: 0, + }) +} + +func balanceToContractData(assetContractId, holderID [32]byte, amt xdr.Int128Parts) xdr.LedgerEntryData { + holder := xdr.Hash(holderID) + addressObj := &xdr.ScObject{ + Type: xdr.ScObjectTypeScoAddress, + Address: &xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &holder, + }, + } + keyObj := &xdr.ScObject{ + Type: xdr.ScObjectTypeScoVec, + Vec: &xdr.ScVec{ + xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &balanceMetadataSym}, + xdr.ScVal{ + Type: xdr.ScValTypeScvObject, + Obj: &addressObj, + }, + }, + } + + amountObj := &xdr.ScObject{ + Type: xdr.ScObjectTypeScoI128, + I128: &amt, + } + amountSym := xdr.ScSymbol("amount") + authorizedSym := xdr.ScSymbol("authorized") + clawbackSym := xdr.ScSymbol("clawback") + trueIc := xdr.ScStaticScsTrue + dataObj := &xdr.ScObject{ + Type: xdr.ScObjectTypeScoMap, + Map: &xdr.ScMap{ + xdr.ScMapEntry{ + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &amountSym, + }, + Val: xdr.ScVal{ + Type: xdr.ScValTypeScvObject, + Obj: &amountObj, + }, + }, + xdr.ScMapEntry{ + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &authorizedSym, + }, + Val: xdr.ScVal{ + Type: xdr.ScValTypeScvStatic, + Ic: &trueIc, + }, + }, + xdr.ScMapEntry{ + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &clawbackSym, + }, + Val: xdr.ScVal{ + Type: xdr.ScValTypeScvStatic, + Ic: &trueIc, + }, + }, + }, + } + + return xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.ContractDataEntry{ + ContractId: assetContractId, + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvObject, + Obj: &keyObj, + }, + Val: xdr.ScVal{ + Type: xdr.ScValTypeScvObject, + Obj: &dataObj, + }, + }, + } +} diff --git a/services/horizon/internal/ingest/verify.go b/services/horizon/internal/ingest/verify.go index 195f7ad58a..bd9af9d2d6 100644 --- a/services/horizon/internal/ingest/verify.go +++ b/services/horizon/internal/ingest/verify.go @@ -160,14 +160,6 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { if entryType == xdr.LedgerEntryTypeConfigSetting || entryType == xdr.LedgerEntryTypeContractCode { return true, entry } - if entryType == xdr.LedgerEntryTypeContractData { - asset := processors.AssetFromContractData(entry, s.config.NetworkPassphrase) - if asset == nil { - return true, entry - } - // we don't keep track of last modified ledgers for contract data - entry.LastModifiedLedgerSeq = 0 - } return false, entry }) @@ -191,8 +183,7 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { trustLines := make([]xdr.LedgerKeyTrustLine, 0, verifyBatchSize) cBalances := make([]xdr.ClaimableBalanceId, 0, verifyBatchSize) lPools := make([]xdr.PoolId, 0, verifyBatchSize) - contractIDs := make([][32]byte, 0, verifyBatchSize) - for i, entry := range entries { + for _, entry := range entries { switch entry.Data.Type { case xdr.LedgerEntryTypeAccount: accounts = append(accounts, entry.Data.MustAccount().AccountId.Address()) @@ -216,14 +207,17 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { // contract data is a special case. // we don't store contract data entries in the db, // however, we ingest contract data entries for asset stats. + + if err = verifier.Write(entry); err != nil { + return err + } err = assetStats.AddContractData(ingest.Change{ Type: xdr.LedgerEntryTypeContractData, - Post: &entries[i], + Post: &entry, }) if err != nil { return errors.Wrap(err, "Error running assetStats.AddContractData") } - contractIDs = append(contractIDs, entries[i].Data.MustContractData().ContractId) totalByType["contract_data"]++ default: return errors.New("GetLedgerEntries return unexpected type") @@ -260,11 +254,6 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { return errors.Wrap(err, "addLiquidityPoolsToStateVerifier failed") } - err = addContractIDsToStateVerifier(s.ctx, verifier, historyQ, contractIDs) - if err != nil { - return errors.Wrap(err, "addContractIDsToStateVerifier failed") - } - total += int64(len(entries)) localLog.WithField("total", total).Info("Batch added to StateVerifier") } @@ -301,14 +290,9 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { return errors.Wrap(err, "Error running historyQ.CountLiquidityPools") } - countContractIDs, err := historyQ.CountContractIDs(s.ctx) - if err != nil { - return errors.Wrap(err, "Error running historyQ.CountContractIDs") - } - err = verifier.Verify( countAccounts + countData + countOffers + countTrustLines + countClaimableBalances + - countLiquidityPools + countContractIDs, + countLiquidityPools + int(totalByType["contract_data"]), ) if err != nil { return errors.Wrap(err, "verifier.Verify failed") @@ -603,48 +587,6 @@ func offerToXDR(row history.Offer) xdr.OfferEntry { } } -func addContractIDsToStateVerifier( - ctx context.Context, - verifier *verify.StateVerifier, - q history.IngestionQ, - contractIDs [][32]byte, -) error { - if len(contractIDs) == 0 { - return nil - } - - assets, err := q.GetAssetStatByContracts(ctx, contractIDs) - if err != nil { - return errors.Wrap(err, "Error running q.GetAssetStatByContracts") - } - - for _, asset := range assets { - contractID, ok := asset.GetContractID() - if !ok { - return ingest.NewStateError( - fmt.Errorf("asset %s:%s is missing contract id", asset.AssetCode, asset.AssetIssuer), - ) - } - - data, err := processors.AssetToContractData( - asset.AssetType == xdr.AssetTypeAssetTypeNative, - asset.AssetCode, - asset.AssetIssuer, - contractID, - ) - if err != nil { - return err - } - err = verifier.Write(xdr.LedgerEntry{ - Data: data, - }) - if err != nil { - return err - } - } - return nil -} - func addTrustLinesToStateVerifier( ctx context.Context, verifier *verify.StateVerifier, diff --git a/services/horizon/internal/ingest/verify_range_state_test.go b/services/horizon/internal/ingest/verify_range_state_test.go index abe26dbca6..b18116db1e 100644 --- a/services/horizon/internal/ingest/verify_range_state_test.go +++ b/services/horizon/internal/ingest/verify_range_state_test.go @@ -560,6 +560,7 @@ func (s *VerifyRangeStateTestSuite) TestSuccessWithVerify() { ClaimableBalances: "0", LiquidityPools: "450", Unauthorized: "0", + Contracts: "0", }, Amount: "0", }}, nil).Once() diff --git a/services/horizon/internal/ingest/verify_test.go b/services/horizon/internal/ingest/verify_test.go index c078cf9895..3438e98e6c 100644 --- a/services/horizon/internal/ingest/verify_test.go +++ b/services/horizon/internal/ingest/verify_test.go @@ -211,10 +211,14 @@ func genAssetContractMetadata(tt *test.T, gen randxdr.Generator) []xdr.LedgerEnt otherTrustline := genTrustLine(tt, gen, assetPreset) otherAssetContractMetadata := assetContractMetadataFromTrustline(tt, otherTrustline) + return []xdr.LedgerEntryChange{ assetContractMetadata, trustline, + balanceContractDataFromTrustline(tt, trustline), otherAssetContractMetadata, + balanceContractDataFromTrustline(tt, otherTrustline), + balanceContractDataFromTrustline(tt, genTrustLine(tt, gen, assetPreset)), } } @@ -238,6 +242,25 @@ func assetContractMetadataFromTrustline(tt *test.T, trustline xdr.LedgerEntryCha return assetContractMetadata } +func balanceContractDataFromTrustline(tt *test.T, trustline xdr.LedgerEntryChange) xdr.LedgerEntryChange { + contractID, err := trustline.Created.Data.MustTrustLine().Asset.ToAsset().ContractID("") + tt.Assert.NoError(err) + var assetType xdr.AssetType + var code, issuer string + trustlineData := trustline.Created.Data.MustTrustLine() + tt.Assert.NoError( + trustlineData.Asset.Extract(&assetType, &code, &issuer), + ) + assetContractMetadata := xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: trustline.Created.LastModifiedLedgerSeq, + Data: processors.BalanceToContractData(contractID, *trustlineData.AccountId.Ed25519, uint64(trustlineData.Balance)), + }, + } + return assetContractMetadata +} + func TestStateVerifier(t *testing.T) { tt := test.Start(t) defer tt.Finish() diff --git a/services/horizon/internal/integration/sac_test.go b/services/horizon/internal/integration/sac_test.go index bb25153352..39a963a564 100644 --- a/services/horizon/internal/integration/sac_test.go +++ b/services/horizon/internal/integration/sac_test.go @@ -2,8 +2,9 @@ package integration import ( "context" - "crypto/sha256" - + "math" + "math/big" + "strings" "testing" "github.com/stellar/go/amount" @@ -54,7 +55,15 @@ func TestContractMintToAccount(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("20")) - assertAssetStats(itest, issuer, code, 1, amount.MustParse("20"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("20"), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) otherRecipientKp, otherRecipient := itest.CreateAccount("100") itest.MustEstablishTrustline(otherRecipientKp, otherRecipient, txnbuild.MustAssetFromXDR(asset)) @@ -67,7 +76,15 @@ func TestContractMintToAccount(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("20")) assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30")) - assertAssetStats(itest, issuer, code, 2, amount.MustParse("50"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 2, + balanceAccounts: amount.MustParse("50"), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) } func TestContractMintToContract(t *testing.T) { @@ -92,7 +109,7 @@ func TestContractMintToContract(t *testing.T) { assertInvokeHostFnSucceeds( itest, itest.Master(), - mint(itest, issuer, asset, "20", contractAddressParam(recipientContractID)), + mintWithAmt(itest, issuer, asset, i128Param(math.MaxInt64, math.MaxUint64-3), contractAddressParam(recipientContractID)), ) balanceAmount := assertInvokeHostFnSucceeds( @@ -103,15 +120,14 @@ func TestContractMintToContract(t *testing.T) { assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvObject, balanceAmount.Type) assert.Equal(itest.CurrentTest(), xdr.ScObjectTypeScoI128, (*balanceAmount.Obj).Type) - // The quantities are correct, (they are multiplied by 10^7 because we converted the amounts to stroops) - assert.Equal(itest.CurrentTest(), xdr.Uint64(200000000), (*balanceAmount.Obj).I128.Lo) - assert.Equal(itest.CurrentTest(), xdr.Uint64(0), (*balanceAmount.Obj).I128.Hi) + assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64-3), (*balanceAmount.Obj).I128.Lo) + assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxInt64), (*balanceAmount.Obj).I128.Hi) // calling xfer from the issuer account will also mint the asset assertInvokeHostFnSucceeds( itest, itest.Master(), - xfer(itest, issuer, asset, "30", contractAddressParam(recipientContractID)), + xferWithAmount(itest, issuer, asset, i128Param(0, 3), contractAddressParam(recipientContractID)), ) balanceAmount = assertInvokeHostFnSucceeds( @@ -120,9 +136,20 @@ func TestContractMintToContract(t *testing.T) { balance(itest, issuer, asset, contractAddressParam(recipientContractID)), ) - assert.Equal(itest.CurrentTest(), xdr.Uint64(500000000), (*balanceAmount.Obj).I128.Lo) - assert.Equal(itest.CurrentTest(), xdr.Uint64(0), (*balanceAmount.Obj).I128.Hi) - assertAssetStats(itest, issuer, code, 0, amount.MustParse("0"), stellarAssetContractID(itest, asset)) + assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64), (*balanceAmount.Obj).I128.Lo) + assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxInt64), (*balanceAmount.Obj).I128.Hi) + // balanceContracts = 2^127 - 1 + balanceContracts := new(big.Int).Lsh(big.NewInt(1), 127) + balanceContracts.Sub(balanceContracts, big.NewInt(1)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + numContracts: 1, + balanceContracts: balanceContracts, + contractID: stellarAssetContractID(itest, asset), + }) } func TestContractTransferBetweenAccounts(t *testing.T) { @@ -159,7 +186,15 @@ func TestContractTransferBetweenAccounts(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) - assertAssetStats(itest, issuer, code, 1, amount.MustParse("1000"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1000"), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) otherRecipientKp, otherRecipient := itest.CreateAccount("100") itest.MustEstablishTrustline(otherRecipientKp, otherRecipient, txnbuild.MustAssetFromXDR(asset)) @@ -172,11 +207,18 @@ func TestContractTransferBetweenAccounts(t *testing.T) { assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970")) assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30")) - assertAssetStats(itest, issuer, code, 2, amount.MustParse("1000"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 2, + balanceAccounts: amount.MustParse("1000"), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) } func TestContractTransferBetweenAccountAndContract(t *testing.T) { - if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } @@ -226,7 +268,15 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { mint(itest, issuer, asset, "1000", contractAddressParam(recipientContractID)), ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) - assertAssetStats(itest, issuer, code, 1, amount.MustParse("1000"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1000"), + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), + contractID: stellarAssetContractID(itest, asset), + }) // transfer from account to contract assertInvokeHostFnSucceeds( @@ -235,16 +285,32 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { xfer(itest, recipientKp.Address(), asset, "30", contractAddressParam(recipientContractID)), ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970")) - assertAssetStats(itest, issuer, code, 1, amount.MustParse("970"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("970"), + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("1030"))), + contractID: stellarAssetContractID(itest, asset), + }) // transfer from contract to account assertInvokeHostFnSucceeds( itest, recipientKp, - xferFromContract(itest, recipientKp.Address(), recipientContractID, asset, "500", accountAddressParam(recipient.GetAccountID())), + xferFromContract(itest, recipientKp.Address(), recipientContractID, "500", accountAddressParam(recipient.GetAccountID())), ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1470")) - assertAssetStats(itest, issuer, code, 1, amount.MustParse("1470"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1470"), + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("530"))), + contractID: stellarAssetContractID(itest, asset), + }) balanceAmount := assertInvokeHostFnSucceeds( itest, @@ -295,7 +361,7 @@ func TestContractTransferBetweenContracts(t *testing.T) { assertInvokeHostFnSucceeds( itest, itest.Master(), - xferFromContract(itest, issuer, emitterContractID, asset, "10", contractAddressParam(recipientContractID)), + xferFromContract(itest, issuer, emitterContractID, "10", contractAddressParam(recipientContractID)), ) // Check balances of emitter and recipient @@ -317,8 +383,15 @@ func TestContractTransferBetweenContracts(t *testing.T) { assert.Equal(itest.CurrentTest(), xdr.Uint64(100000000), (*recipientBalanceAmount.Obj).I128.Lo) assert.Equal(itest.CurrentTest(), xdr.Uint64(0), (*recipientBalanceAmount.Obj).I128.Hi) - assertAssetStats(itest, issuer, code, 0, amount.MustParse("0"), stellarAssetContractID(itest, asset)) - + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + numContracts: 2, + balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), + contractID: stellarAssetContractID(itest, asset), + }) } func TestContractBurnFromAccount(t *testing.T) { @@ -355,7 +428,15 @@ func TestContractBurnFromAccount(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) - assertAssetStats(itest, issuer, code, 1, amount.MustParse("1000"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1000"), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) assertInvokeHostFnSucceeds( itest, @@ -363,6 +444,15 @@ func TestContractBurnFromAccount(t *testing.T) { burn(itest, recipientKp.Address(), asset, "500"), ) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("500"), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) } func TestContractBurnFromContract(t *testing.T) { @@ -413,7 +503,15 @@ func TestContractBurnFromContract(t *testing.T) { assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.Obj).I128.Lo) assert.Equal(itest.CurrentTest(), xdr.Uint64(0), (*balanceAmount.Obj).I128.Hi) - assertAssetStats(itest, issuer, code, 0, amount.MustParse("0"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("990"))), + contractID: stellarAssetContractID(itest, asset), + }) } func TestContractClawbackFromAccount(t *testing.T) { @@ -460,7 +558,15 @@ func TestContractClawbackFromAccount(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) - assertAssetStats(itest, issuer, code, 1, amount.MustParse("1000"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1000"), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) assertInvokeHostFnSucceeds( itest, @@ -468,8 +574,16 @@ func TestContractClawbackFromAccount(t *testing.T) { clawback(itest, issuer, asset, "1000", accountAddressParam(recipientKp.Address())), ) - assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("0")) - assertAssetStats(itest, issuer, code, 1, amount.MustParse("0"), stellarAssetContractID(itest, asset)) + assertContainsBalance(itest, recipientKp, issuer, code, 0) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: 0, + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) } func TestContractClawbackFromContract(t *testing.T) { @@ -523,7 +637,15 @@ func TestContractClawbackFromContract(t *testing.T) { assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.Obj).I128.Lo) assert.Equal(itest.CurrentTest(), xdr.Uint64(0), (*balanceAmount.Obj).I128.Hi) - assertAssetStats(itest, issuer, code, 0, amount.MustParse("0"), stellarAssetContractID(itest, asset)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("990"))), + contractID: stellarAssetContractID(itest, asset), + }) } func assertContainsBalance(itest *integration.Test, acct *keypair.Full, issuer, code string, amt xdr.Int64) { @@ -538,31 +660,44 @@ func assertContainsBalance(itest *integration.Test, acct *keypair.Full, issuer, } } -func assertAssetStats(itest *integration.Test, issuer, code string, numAccounts int32, amt xdr.Int64, contractID [32]byte) { +type assetStats struct { + code string + issuer string + numAccounts int32 + balanceAccounts xdr.Int64 + numContracts int32 + balanceContracts *big.Int + contractID [32]byte +} + +func assertAssetStats(itest *integration.Test, expected assetStats) { assets, err := itest.Client().Assets(horizonclient.AssetRequest{ - ForAssetCode: code, - ForAssetIssuer: issuer, + ForAssetCode: expected.code, + ForAssetIssuer: expected.issuer, Limit: 1, }) assert.NoError(itest.CurrentTest(), err) - for _, asset := range assets.Embedded.Records { - if asset.Issuer != issuer || asset.Code != code { - continue - } - assert.Equal(itest.CurrentTest(), numAccounts, asset.NumAccounts) - assert.Equal(itest.CurrentTest(), numAccounts, asset.Accounts.Authorized) - assert.Equal(itest.CurrentTest(), amt, amount.MustParse(asset.Amount)) - assert.Equal(itest.CurrentTest(), strkey.MustEncode(strkey.VersionByteContract, contractID[:]), asset.ContractID) + + if expected.numContracts == 0 && expected.numAccounts == 0 && + expected.balanceContracts.Cmp(big.NewInt(0)) == 0 && expected.balanceAccounts == 0 { + assert.Empty(itest.CurrentTest(), assets) return } - if numAccounts != 0 || amt != 0 { - itest.CurrentTest().Fatalf("could not find balance for aset %s:%s", code, issuer) - } -} -func masterAccountIDEnumParam(itest *integration.Test) xdr.ScVal { - root := keypair.Root(itest.GetPassPhrase()) - return accountAddressParam(root.Address()) + assert.Len(itest.CurrentTest(), assets.Embedded.Records, 1) + asset := assets.Embedded.Records[0] + assert.Equal(itest.CurrentTest(), expected.code, asset.Code) + assert.Equal(itest.CurrentTest(), expected.issuer, asset.Issuer) + assert.Equal(itest.CurrentTest(), expected.numAccounts, asset.NumAccounts) + assert.Equal(itest.CurrentTest(), expected.numAccounts, asset.Accounts.Authorized) + assert.Equal(itest.CurrentTest(), expected.balanceAccounts, amount.MustParse(asset.Amount)) + assert.Equal(itest.CurrentTest(), expected.numContracts, asset.NumContracts) + parts := strings.Split(asset.ContractsAmount, ".") + assert.Len(itest.CurrentTest(), parts, 2) + contractsAmount, ok := new(big.Int).SetString(parts[0]+parts[1], 10) + assert.True(itest.CurrentTest(), ok) + assert.Equal(itest.CurrentTest(), expected.balanceContracts.String(), contractsAmount.String()) + assert.Equal(itest.CurrentTest(), strkey.MustEncode(strkey.VersionByteContract, expected.contractID[:]), asset.ContractID) } func functionNameParam(name string) xdr.ScVal { @@ -646,6 +781,10 @@ func createSAC(itest *integration.Test, sourceAccount string, asset xdr.Asset) * } func mint(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetAmount string, recipient xdr.ScVal) *txnbuild.InvokeHostFunction { + return mintWithAmt(itest, sourceAccount, asset, i128Param(0, uint64(amount.MustParse(assetAmount))), recipient) +} + +func mintWithAmt(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetAmount xdr.ScVal, recipient xdr.ScVal) *txnbuild.InvokeHostFunction { invokeHostFn := addFootprint(itest, &txnbuild.InvokeHostFunction{ Function: xdr.HostFunction{ Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, @@ -654,7 +793,7 @@ func mint(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetA functionNameParam("mint"), accountAddressParam(sourceAccount), recipient, - i128Param(0, uint64(amount.MustParse(assetAmount))), + assetAmount, }, }, SourceAccount: sourceAccount, @@ -666,7 +805,7 @@ func mint(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetA xdr.ScVec{ accountAddressParam(sourceAccount), recipient, - i128Param(0, uint64(amount.MustParse(assetAmount))), + assetAmount, }) return invokeHostFn @@ -737,6 +876,16 @@ func balance(itest *integration.Test, sourceAccount string, asset xdr.Asset, hol } func xfer(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetAmount string, recipient xdr.ScVal) *txnbuild.InvokeHostFunction { + return xferWithAmount( + itest, + sourceAccount, + asset, + i128Param(0, uint64(amount.MustParse(assetAmount))), + recipient, + ) +} + +func xferWithAmount(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetAmount xdr.ScVal, recipient xdr.ScVal) *txnbuild.InvokeHostFunction { invokeHostFn := addFootprint(itest, &txnbuild.InvokeHostFunction{ Function: xdr.HostFunction{ Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, @@ -745,7 +894,7 @@ func xfer(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetA functionNameParam("xfer"), accountAddressParam(sourceAccount), recipient, - i128Param(0, uint64(amount.MustParse(assetAmount))), + assetAmount, }, }, SourceAccount: sourceAccount, @@ -757,7 +906,7 @@ func xfer(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetA xdr.ScVec{ accountAddressParam(sourceAccount), recipient, - i128Param(0, uint64(amount.MustParse(assetAmount))), + assetAmount, }) return invokeHostFn @@ -787,7 +936,7 @@ func burnSelf(itest *integration.Test, sourceAccount string, sacTestcontractID x return invokeHostFn } -func xferFromContract(itest *integration.Test, sourceAccount string, sacTestcontractID xdr.Hash, asset xdr.Asset, assetAmount string, recipient xdr.ScVal) *txnbuild.InvokeHostFunction { +func xferFromContract(itest *integration.Test, sourceAccount string, sacTestcontractID xdr.Hash, assetAmount string, recipient xdr.ScVal) *txnbuild.InvokeHostFunction { invokeHostFn := addFootprint(itest, &txnbuild.InvokeHostFunction{ Function: xdr.HostFunction{ Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, @@ -886,17 +1035,9 @@ func assertInvokeHostFnSucceeds(itest *integration.Test, signer *keypair.Full, o } func stellarAssetContractID(itest *integration.Test, asset xdr.Asset) xdr.Hash { - networkId := xdr.Hash(sha256.Sum256([]byte(itest.GetPassPhrase()))) - preImage := xdr.HashIdPreimage{ - Type: xdr.EnvelopeTypeEnvelopeTypeContractIdFromAsset, - FromAsset: &xdr.HashIdPreimageFromAsset{ - NetworkId: networkId, - Asset: asset, - }, - } - xdrPreImageBytes, err := preImage.MarshalBinary() + contractID, err := asset.ContractID(itest.GetPassPhrase()) require.NoError(itest.CurrentTest(), err) - return sha256.Sum256(xdrPreImageBytes) + return contractID } func addAuthNextInvokerFlow(fnName string, contractId xdr.Hash, args xdr.ScVec) []xdr.ContractAuth { diff --git a/services/horizon/internal/resourceadapter/asset_stat.go b/services/horizon/internal/resourceadapter/asset_stat.go index 141c793831..89196e9c89 100644 --- a/services/horizon/internal/resourceadapter/asset_stat.go +++ b/services/horizon/internal/resourceadapter/asset_stat.go @@ -37,6 +37,7 @@ func PopulateAssetStat( } res.NumClaimableBalances = row.Accounts.ClaimableBalances res.NumLiquidityPools = row.Accounts.LiquidityPools + res.NumContracts = row.Accounts.Contracts res.NumAccounts = row.NumAccounts err = populateAssetStatBalances(res, row.Balances) if err != nil { @@ -91,5 +92,10 @@ func populateAssetStatBalances(res *protocol.AssetStat, row history.ExpAssetStat return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.LiquidityPools) } + res.ContractsAmount, err = amount.IntStringToAmount(row.Contracts) + if err != nil { + return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Contracts) + } + return nil } diff --git a/services/horizon/internal/resourceadapter/asset_stat_test.go b/services/horizon/internal/resourceadapter/asset_stat_test.go index 1b17d125f0..b1530f88b6 100644 --- a/services/horizon/internal/resourceadapter/asset_stat_test.go +++ b/services/horizon/internal/resourceadapter/asset_stat_test.go @@ -21,6 +21,7 @@ func TestPopulateExpAssetStat(t *testing.T) { AuthorizedToMaintainLiabilities: 214, Unauthorized: 107, ClaimableBalances: 12, + Contracts: 6, }, Balances: history.ExpAssetStatBalances{ Authorized: "100000000000000000000", @@ -28,6 +29,7 @@ func TestPopulateExpAssetStat(t *testing.T) { Unauthorized: "2500000000000000000", ClaimableBalances: "1200000000000000000", LiquidityPools: "7700000000000000000", + Contracts: "900000000000000000", }, Amount: "100000000000000000000", // 10T NumAccounts: 429, @@ -49,11 +51,13 @@ func TestPopulateExpAssetStat(t *testing.T) { assert.Equal(t, int32(214), res.Accounts.AuthorizedToMaintainLiabilities) assert.Equal(t, int32(107), res.Accounts.Unauthorized) assert.Equal(t, int32(12), res.NumClaimableBalances) + assert.Equal(t, int32(6), res.NumContracts) assert.Equal(t, "10000000000000.0000000", res.Balances.Authorized) assert.Equal(t, "5000000000000.0000000", res.Balances.AuthorizedToMaintainLiabilities) assert.Equal(t, "250000000000.0000000", res.Balances.Unauthorized) assert.Equal(t, "120000000000.0000000", res.ClaimableBalancesAmount) assert.Equal(t, "770000000000.0000000", res.LiquidityPoolsAmount) + assert.Equal(t, "90000000000.0000000", res.ContractsAmount) assert.Equal(t, "10000000000000.0000000", res.Amount) assert.Equal(t, int32(429), res.NumAccounts) assert.Equal(t, horizon.AccountFlags{}, res.Flags) diff --git a/xdr/scval.go b/xdr/scval.go index 6ad2ca694e..7bf31e2f1d 100644 --- a/xdr/scval.go +++ b/xdr/scval.go @@ -155,3 +155,9 @@ func (s ScAddress) Equals(o ScAddress) bool { panic("unknown ScAddress type: " + s.Type.String()) } } + +// IsBool returns true if the given ScVal is a boolean +func (s ScVal) IsBool() bool { + ic, ok := s.GetIc() + return ok && (ic == ScStaticScsTrue || ic == ScStaticScsFalse) +}