From cd3a221ac71ef43e81de53fe42206862823c415b Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 16 Mar 2023 08:34:31 +0000 Subject: [PATCH] services/horizon: Include contract asset balances in asset stats (#4805) This PR a follow up to #4782 where we update asset stats ingestion to keep track of how many contracts hold an asset and the amount of an asset held by all contracts in total. This is implemented by ingesting balance contract data ledger entries which are updated by the stellar asset contract. The balance contract data ledger entries are like trustlines but for contract holders of an asset. --- protocols/horizon/main.go | 2 + .../horizon/internal/actions/asset_test.go | 5 + .../internal/db2/history/asset_stats.go | 14 - .../internal/db2/history/asset_stats_test.go | 48 ++- services/horizon/internal/db2/history/main.go | 18 +- .../processors/asset_stats_processor.go | 122 +++++- .../processors/asset_stats_processor_test.go | 389 ++++++++++++++++++ .../ingest/processors/asset_stats_set.go | 155 ++++++- .../ingest/processors/asset_stats_set_test.go | 273 +++++++++++- .../ingest/processors/contract_data.go | 247 ++++++++--- services/horizon/internal/ingest/verify.go | 72 +--- .../ingest/verify_range_state_test.go | 1 + .../horizon/internal/ingest/verify_test.go | 23 ++ .../horizon/internal/integration/sac_test.go | 263 +++++++++--- .../internal/resourceadapter/asset_stat.go | 6 + .../resourceadapter/asset_stat_test.go | 4 + xdr/scval.go | 6 + 17 files changed, 1412 insertions(+), 236 deletions(-) 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) +}