diff --git a/api/disabled_parameters.go b/api/disabled_parameters.go index 33fd3eaed..a0623bbc3 100644 --- a/api/disabled_parameters.go +++ b/api/disabled_parameters.go @@ -98,7 +98,7 @@ func GetDefaultDisabledMapConfigForPostgres() *DisabledMapConfig { get("/v2/accounts", []string{"currency-greater-than", "currency-less-than"}) get("/v2/accounts/{account-id}/transactions", []string{"note-prefix", "tx-type", "sig-type", "asset-id", "before-time", "after-time", "rekey-to"}) get("/v2/assets", []string{"name", "unit"}) - get("/v2/assets/{asset-id}/balances", []string{"round", "currency-greater-than", "currency-less-than"}) + get("/v2/assets/{asset-id}/balances", []string{"currency-greater-than", "currency-less-than"}) get("/v2/transactions", []string{"note-prefix", "tx-type", "sig-type", "asset-id", "before-time", "after-time", "currency-greater-than", "currency-less-than", "address-role", "exclude-close-to", "rekey-to", "application-id"}) get("/v2/assets/{asset-id}/transactions", []string{"note-prefix", "tx-type", "sig-type", "asset-id", "before-time", "after-time", "currency-greater-than", "currency-less-than", "address-role", "exclude-close-to", "rekey-to"}) diff --git a/api/handlers_e2e_test.go b/api/handlers_e2e_test.go index fa80e0415..fbbaee02a 100644 --- a/api/handlers_e2e_test.go +++ b/api/handlers_e2e_test.go @@ -351,22 +351,40 @@ func TestAccountMaxResultsLimit(t *testing.T) { defer shutdownFunc() /////////// - // Given // A block containing an address that has created 5 apps and 5 assets, and another address that has opted into them all + // Given // A block containing an address that has created 10 apps, deleted 5 apps, and created 10 assets, + // // deleted 5 assets, and another address that has opted into the 5 apps and 5 assets remaining /////////// - expectedAppIDs := []uint64{1, 2, 3, 4, 5} - expectedAssetIDs := []uint64{6, 7, 8, 9, 10} + deletedAppIDs := []uint64{1, 2, 3, 4, 5} + deletedAssetIDs := []uint64{6, 7, 8, 9, 10} + expectedAppIDs := []uint64{11, 12, 13, 14, 15} + expectedAssetIDs := []uint64{16, 17, 18, 19, 20} var txns []transactions.SignedTxnWithAD + // make apps and assets + for range deletedAppIDs { + txns = append(txns, test.MakeCreateAppTxn(test.AccountA)) + } + for _, id := range deletedAssetIDs { + txns = append(txns, test.MakeAssetConfigTxn(0, 100, 0, false, "UNIT", + fmt.Sprintf("Asset %d", id), "http://asset.com", test.AccountA)) + } for range expectedAppIDs { txns = append(txns, test.MakeCreateAppTxn(test.AccountA)) } - - for i := range expectedAssetIDs { + for _, id := range expectedAssetIDs { txns = append(txns, test.MakeAssetConfigTxn(0, 100, 0, false, "UNIT", - fmt.Sprintf("Asset %d", expectedAssetIDs[i]), "http://asset.com", test.AccountA)) + fmt.Sprintf("Asset %d", id), "http://asset.com", test.AccountA)) + } + // delete some apps and assets + for _, id := range deletedAppIDs { + txns = append(txns, test.MakeAppDestroyTxn(id, test.AccountA)) + } + for _, id := range deletedAssetIDs { + txns = append(txns, test.MakeAssetDestroyTxn(id, test.AccountA)) } + // opt in to the remaining ones for _, id := range expectedAppIDs { txns = append(txns, test.MakeAppOptInTxn(id, test.AccountA)) txns = append(txns, test.MakeAppOptInTxn(id, test.AccountB)) @@ -418,11 +436,14 @@ func TestAccountMaxResultsLimit(t *testing.T) { require.True(t, serverUp, "api.Serve did not start server in time") // make a real HTTP request (to additionally test generated param parsing logic) - makeReq := func(t *testing.T, path string, exclude []string, next *string, limit *uint64) (*http.Response, []byte) { + makeReq := func(t *testing.T, path string, exclude []string, includeDeleted bool, next *string, limit *uint64) (*http.Response, []byte) { var query []string if len(exclude) > 0 { query = append(query, "exclude="+strings.Join(exclude, ",")) } + if includeDeleted { + query = append(query, "include-all=true") + } if next != nil { query = append(query, "next="+*next) } @@ -466,12 +487,17 @@ func TestAccountMaxResultsLimit(t *testing.T) { } testCases := []struct { - address basics.Address - exclude []string - errStatus int + address basics.Address + exclude []string + includeDeleted bool + errStatus int }{ {address: test.AccountA, exclude: []string{"all"}}, {address: test.AccountA, exclude: []string{"created-assets", "created-apps", "apps-local-state", "assets"}}, + {address: test.AccountA, exclude: []string{"assets", "created-apps"}}, + {address: test.AccountA, exclude: []string{"assets", "apps-local-state"}}, + {address: test.AccountA, exclude: []string{"assets", "apps-local-state"}, includeDeleted: true, errStatus: http.StatusBadRequest}, + {address: test.AccountB, exclude: []string{"created-assets", "apps-local-state"}}, {address: test.AccountB, exclude: []string{"assets", "apps-local-state"}}, {address: test.AccountA, exclude: []string{"created-assets"}, errStatus: http.StatusBadRequest}, {address: test.AccountA, exclude: []string{"created-apps"}, errStatus: http.StatusBadRequest}, @@ -483,18 +509,23 @@ func TestAccountMaxResultsLimit(t *testing.T) { maxResults := 14 t.Run(fmt.Sprintf("LookupAccountByID exclude %v", tc.exclude), func(t *testing.T) { path := "/v2/accounts/" + tc.address.String() - resp, data := makeReq(t, path, tc.exclude, nil, nil) + resp, data := makeReq(t, path, tc.exclude, tc.includeDeleted, nil, nil) if tc.errStatus != 0 { // was a 400 error expected? check error response require.Equal(t, tc.errStatus, resp.StatusCode) var response generated.AccountsErrorResponse err = json.Decode(data, &response) require.NoError(t, err) - assert.Equal(t, *response.Address, tc.address.String()) - assert.Equal(t, *response.MaxResults, uint64(maxResults)) - assert.Equal(t, *response.TotalAppsOptedIn, uint64(5)) - assert.Equal(t, *response.TotalCreatedApps, uint64(5)) - assert.Equal(t, *response.TotalAssetsOptedIn, uint64(5)) - assert.Equal(t, *response.TotalCreatedAssets, uint64(5)) + assert.Equal(t, tc.address.String(), *response.Address) + assert.Equal(t, uint64(maxResults), *response.MaxResults) + if tc.includeDeleted { + assert.Equal(t, uint64(10), *response.TotalCreatedApps) + assert.Equal(t, uint64(10), *response.TotalCreatedAssets) + } else { + assert.Equal(t, uint64(5), *response.TotalAppsOptedIn) + assert.Equal(t, uint64(5), *response.TotalAssetsOptedIn) + assert.Equal(t, uint64(5), *response.TotalCreatedApps) + assert.Equal(t, uint64(5), *response.TotalCreatedAssets) + } return } require.Equal(t, http.StatusOK, resp.StatusCode, fmt.Sprintf("unexpected return code, body: %s", string(data))) @@ -526,7 +557,7 @@ func TestAccountMaxResultsLimit(t *testing.T) { } { t.Run(fmt.Sprintf("SearchForAccounts exclude %v", tc.exclude), func(t *testing.T) { maxResults := 14 - resp, data := makeReq(t, "/v2/accounts", tc.exclude, nil, nil) + resp, data := makeReq(t, "/v2/accounts", tc.exclude, false, nil, nil) if tc.errStatus != 0 { // was a 400 error expected? check error response require.Equal(t, tc.errStatus, resp.StatusCode) var response generated.AccountsErrorResponse @@ -577,7 +608,7 @@ func TestAccountMaxResultsLimit(t *testing.T) { limit := uint64(2) // 2 at a time var assets []generated.AssetHolding for { - resp, data := makeReq(t, "/v2/accounts/"+test.AccountB.String()+"/assets", nil, next, &limit) + resp, data := makeReq(t, "/v2/accounts/"+test.AccountB.String()+"/assets", nil, false, next, &limit) require.Equal(t, http.StatusOK, resp.StatusCode, fmt.Sprintf("unexpected return code, body: %s", string(data))) var response generated.AssetHoldingsResponse err = json.Decode(data, &response) @@ -608,7 +639,7 @@ func TestAccountMaxResultsLimit(t *testing.T) { limit := uint64(2) // 2 at a time var assets []generated.Asset for { - resp, data := makeReq(t, "/v2/accounts/"+test.AccountA.String()+"/created-assets", nil, next, &limit) + resp, data := makeReq(t, "/v2/accounts/"+test.AccountA.String()+"/created-assets", nil, false, next, &limit) require.Equal(t, http.StatusOK, resp.StatusCode, fmt.Sprintf("unexpected return code, body: %s", string(data))) var response generated.AssetsResponse err = json.Decode(data, &response) @@ -639,7 +670,7 @@ func TestAccountMaxResultsLimit(t *testing.T) { limit := uint64(2) // 2 at a time var apps []generated.ApplicationLocalState for { - resp, data := makeReq(t, "/v2/accounts/"+test.AccountA.String()+"/apps-local-state", nil, next, &limit) + resp, data := makeReq(t, "/v2/accounts/"+test.AccountA.String()+"/apps-local-state", nil, false, next, &limit) require.Equal(t, http.StatusOK, resp.StatusCode, fmt.Sprintf("unexpected return code, body: %s", string(data))) var response generated.ApplicationLocalStatesResponse err = json.Decode(data, &response) @@ -670,7 +701,7 @@ func TestAccountMaxResultsLimit(t *testing.T) { limit := uint64(2) // 2 at a time var apps []generated.Application for { - resp, data := makeReq(t, "/v2/accounts/"+test.AccountA.String()+"/created-applications", nil, next, &limit) + resp, data := makeReq(t, "/v2/accounts/"+test.AccountA.String()+"/created-applications", nil, false, next, &limit) require.Equal(t, http.StatusOK, resp.StatusCode, fmt.Sprintf("unexpected return code, body: %s", string(data))) var response generated.ApplicationsResponse err = json.Decode(data, &response) @@ -794,7 +825,8 @@ func TestInnerTxn(t *testing.T) { ////////// require.Equal(t, http.StatusOK, rec.Code) var response generated.TransactionsResponse - json.Decode(rec.Body.Bytes(), &response) + err = json.Decode(rec.Body.Bytes(), &response) + require.NoError(t, err) require.Len(t, response.Transactions, 1) require.Equal(t, expectedID, *(response.Transactions[0].Id)) @@ -862,7 +894,8 @@ func TestPagingRootTxnDeduplication(t *testing.T) { require.Equal(t, http.StatusOK, rec1.Code) var response generated.TransactionsResponse - json.Decode(rec1.Body.Bytes(), &response) + err = json.Decode(rec1.Body.Bytes(), &response) + require.NoError(t, err) require.Len(t, response.Transactions, 1) require.Equal(t, expectedID, *(response.Transactions[0].Id)) pageOneNextToken := *response.NextToken @@ -884,7 +917,8 @@ func TestPagingRootTxnDeduplication(t *testing.T) { ////////// var response2 generated.TransactionsResponse require.Equal(t, http.StatusOK, rec2.Code) - json.Decode(rec2.Body.Bytes(), &response2) + err = json.Decode(rec2.Body.Bytes(), &response2) + require.NoError(t, err) require.Len(t, response2.Transactions, 0) // The fact that NextToken changes indicates that the search results were different. @@ -916,7 +950,8 @@ func TestPagingRootTxnDeduplication(t *testing.T) { ////////// var response generated.BlockResponse require.Equal(t, http.StatusOK, rec.Code) - json.Decode(rec.Body.Bytes(), &response) + err = json.Decode(rec.Body.Bytes(), &response) + require.NoError(t, err) require.NotNil(t, response.Transactions) require.Len(t, *response.Transactions, 1) diff --git a/idb/postgres/postgres.go b/idb/postgres/postgres.go index 2d89a6b58..b4fc76822 100644 --- a/idb/postgres/postgres.go +++ b/idb/postgres/postgres.go @@ -1696,7 +1696,7 @@ func (db *IndexerDb) GetAccounts(ctx context.Context, opts idb.AccountQueryOptio } // Construct query for fetching accounts... - query, whereArgs := db.buildAccountQuery(opts) + query, whereArgs := db.buildAccountQuery(opts, false) req := &getAccountsRequest{ opts: opts, blockheader: blockheader, @@ -1724,16 +1724,25 @@ func (db *IndexerDb) checkAccountResourceLimit(ctx context.Context, tx pgx.Tx, o // skip check if no resources are requested if !opts.IncludeAssetHoldings && !opts.IncludeAssetParams && !opts.IncludeAppLocalState && !opts.IncludeAppParams { return nil - } - // build query based on copy of provided filter options, but with no resources, to count total # that would be returned + // make a copy of the filters requested o := opts - o.IncludeAssetHoldings = false - o.IncludeAssetParams = false - o.IncludeAppLocalState = false - o.IncludeAppParams = false - query, whereArgs := db.buildAccountQuery(o) + var countOnly bool + + if opts.IncludeDeleted { + // if IncludeDeleted is set, need to construct a query (preserving filters) to count deleted values that would be returned from + // asset, app, account_asset, account_app + countOnly = true + } else { + // if IncludeDeleted is not set, query AccountData with no resources (preserving filters), to read ad.TotalX counts inside + o.IncludeAssetHoldings = false + o.IncludeAssetParams = false + o.IncludeAppLocalState = false + o.IncludeAppParams = false + } + + query, whereArgs := db.buildAccountQuery(o, countOnly) rows, err := tx.Query(ctx, query, whereArgs...) if err != nil { return fmt.Errorf("account limit query %#v err %v", query, err) @@ -1749,7 +1758,22 @@ func (db *IndexerDb) checkAccountResourceLimit(ctx context.Context, tx pgx.Tx, o var rewardsbase uint64 var keytype *string var accountDataJSONStr []byte + var holdingCount, assetCount, appCount, lsCount uint64 cols := []interface{}{&addr, µalgos, &rewardstotal, &createdat, &closedat, &deleted, &rewardsbase, &keytype, &accountDataJSONStr} + if countOnly { + if o.IncludeAssetHoldings { + cols = append(cols, &holdingCount) + } + if o.IncludeAssetParams { + cols = append(cols, &assetCount) + } + if o.IncludeAppParams { + cols = append(cols, &appCount) + } + if o.IncludeAppLocalState { + cols = append(cols, &lsCount) + } + } err := rows.Scan(cols...) if err != nil { return fmt.Errorf("account limit scan err %v", err) @@ -1762,35 +1786,46 @@ func (db *IndexerDb) checkAccountResourceLimit(ctx context.Context, tx pgx.Tx, o } // check limit against filters (only count what would be returned) - var resultCount uint64 + var resultCount, totalAssets, totalAssetParams, totalAppLocalStates, totalAppParams uint64 + if countOnly { + totalAssets = holdingCount + totalAssetParams = assetCount + totalAppLocalStates = lsCount + totalAppParams = appCount + } else { + totalAssets = ad.TotalAssets + totalAssetParams = ad.TotalAssetParams + totalAppLocalStates = ad.TotalAppLocalStates + totalAppParams = ad.TotalAppParams + } if opts.IncludeAssetHoldings { - resultCount += ad.TotalAssets + resultCount += totalAssets } if opts.IncludeAssetParams { - resultCount += ad.TotalAssetParams + resultCount += totalAssetParams } if opts.IncludeAppLocalState { - resultCount += ad.TotalAppLocalStates + resultCount += totalAppLocalStates } if opts.IncludeAppParams { - resultCount += ad.TotalAppParams + resultCount += totalAppParams } if resultCount > opts.MaxResources { var aaddr basics.Address copy(aaddr[:], addr) return idb.MaxAccountNestedObjectsError{ Address: aaddr, - TotalAppLocalStates: ad.TotalAppLocalStates, - TotalAppParams: ad.TotalAppParams, - TotalAssets: ad.TotalAssets, - TotalAssetParams: ad.TotalAssetParams, + TotalAppLocalStates: totalAppLocalStates, + TotalAppParams: totalAppParams, + TotalAssets: totalAssets, + TotalAssetParams: totalAssetParams, } } } return nil } -func (db *IndexerDb) buildAccountQuery(opts idb.AccountQueryOptions) (query string, whereArgs []interface{}) { +func (db *IndexerDb) buildAccountQuery(opts idb.AccountQueryOptions, countOnly bool) (query string, whereArgs []interface{}) { // Construct query for fetching accounts... const maxWhereParts = 9 whereParts := make([]string, 0, maxWhereParts) @@ -1869,51 +1904,86 @@ func (db *IndexerDb) buildAccountQuery(opts idb.AccountQueryOptions) (query stri withClauses = append(withClauses, "qaccounts AS ("+query+")") query = "WITH " + strings.Join(withClauses, ", ") - if opts.IncludeDeleted { - if opts.IncludeAssetHoldings { - query += `, qaa AS (SELECT xa.addr, json_agg(aa.assetid) as haid, json_agg(aa.amount) as hamt, json_agg(aa.frozen) as hf, json_agg(aa.created_at) as holding_created_at, json_agg(aa.closed_at) as holding_closed_at, json_agg(aa.deleted) as holding_deleted FROM account_asset aa JOIN qaccounts xa ON aa.addr = xa.addr GROUP BY 1)` + + // build nested selects for querying app/asset data associated with an address + if opts.IncludeAssetHoldings { + var where, selectCols string + if !opts.IncludeDeleted { + where = ` WHERE NOT aa.deleted` } - if opts.IncludeAssetParams { - query += `, qap AS (SELECT ya.addr, json_agg(ap.index) as paid, json_agg(ap.params) as pp, json_agg(ap.created_at) as asset_created_at, json_agg(ap.closed_at) as asset_closed_at, json_agg(ap.deleted) as asset_deleted FROM asset ap JOIN qaccounts ya ON ap.creator_addr = ya.addr GROUP BY 1)` + if countOnly { + selectCols = `count(*) as holding_count` + } else { + selectCols = `json_agg(aa.assetid) as haid, json_agg(aa.amount) as hamt, json_agg(aa.frozen) as hf, json_agg(aa.created_at) as holding_created_at, json_agg(aa.closed_at) as holding_closed_at, json_agg(aa.deleted) as holding_deleted` } - if opts.IncludeAppParams { - // app - query += `, qapp AS (SELECT app.creator as addr, json_agg(app.index) as papps, json_agg(app.params) as ppa, json_agg(app.created_at) as app_created_at, json_agg(app.closed_at) as app_closed_at, json_agg(app.deleted) as app_deleted FROM app JOIN qaccounts ON qaccounts.addr = app.creator GROUP BY 1)` + query += `, qaa AS (SELECT xa.addr, ` + selectCols + ` FROM account_asset aa JOIN qaccounts xa ON aa.addr = xa.addr` + where + ` GROUP BY 1)` + } + if opts.IncludeAssetParams { + var where, selectCols string + if !opts.IncludeDeleted { + where = ` WHERE NOT ap.deleted` } - if opts.IncludeAppLocalState { - // app localstate - query += `, qls AS (SELECT la.addr, json_agg(la.app) as lsapps, json_agg(la.localstate) as lsls, json_agg(la.created_at) as ls_created_at, json_agg(la.closed_at) as ls_closed_at, json_agg(la.deleted) as ls_deleted FROM account_app la JOIN qaccounts ON qaccounts.addr = la.addr GROUP BY 1)` + if countOnly { + selectCols = `count(*) as asset_count` + } else { + selectCols = `json_agg(ap.index) as paid, json_agg(ap.params) as pp, json_agg(ap.created_at) as asset_created_at, json_agg(ap.closed_at) as asset_closed_at, json_agg(ap.deleted) as asset_deleted` } - } else { - if opts.IncludeAssetHoldings { - query += `, qaa AS (SELECT xa.addr, json_agg(aa.assetid) as haid, json_agg(aa.amount) as hamt, json_agg(aa.frozen) as hf, json_agg(aa.created_at) as holding_created_at, json_agg(aa.closed_at) as holding_closed_at, json_agg(aa.deleted) as holding_deleted FROM account_asset aa JOIN qaccounts xa ON aa.addr = xa.addr WHERE NOT aa.deleted GROUP BY 1)` + query += `, qap AS (SELECT ya.addr, ` + selectCols + ` FROM asset ap JOIN qaccounts ya ON ap.creator_addr = ya.addr` + where + ` GROUP BY 1)` + } + if opts.IncludeAppParams { + var where, selectCols string + if !opts.IncludeDeleted { + where = ` WHERE NOT app.deleted` } - if opts.IncludeAssetParams { - query += `, qap AS (SELECT ya.addr, json_agg(ap.index) as paid, json_agg(ap.params) as pp, json_agg(ap.created_at) as asset_created_at, json_agg(ap.closed_at) as asset_closed_at, json_agg(ap.deleted) as asset_deleted FROM asset ap JOIN qaccounts ya ON ap.creator_addr = ya.addr WHERE NOT ap.deleted GROUP BY 1)` + if countOnly { + selectCols = `count(*) as app_count` + } else { + selectCols = `json_agg(app.index) as papps, json_agg(app.params) as ppa, json_agg(app.created_at) as app_created_at, json_agg(app.closed_at) as app_closed_at, json_agg(app.deleted) as app_deleted` } - if opts.IncludeAppParams { - // app - query += `, qapp AS (SELECT app.creator as addr, json_agg(app.index) as papps, json_agg(app.params) as ppa, json_agg(app.created_at) as app_created_at, json_agg(app.closed_at) as app_closed_at, json_agg(app.deleted) as app_deleted FROM app JOIN qaccounts ON qaccounts.addr = app.creator WHERE NOT app.deleted GROUP BY 1)` + query += `, qapp AS (SELECT app.creator as addr, ` + selectCols + ` FROM app JOIN qaccounts ON qaccounts.addr = app.creator` + where + ` GROUP BY 1)` + } + if opts.IncludeAppLocalState { + var where, selectCols string + if !opts.IncludeDeleted { + where = ` WHERE NOT la.deleted` } - if opts.IncludeAppLocalState { - // app localstate - query += `, qls AS (SELECT la.addr, json_agg(la.app) as lsapps, json_agg(la.localstate) as lsls, json_agg(la.created_at) as ls_created_at, json_agg(la.closed_at) as ls_closed_at, json_agg(la.deleted) as ls_deleted FROM account_app la JOIN qaccounts ON qaccounts.addr = la.addr WHERE NOT la.deleted GROUP BY 1)` + if countOnly { + selectCols = `count(*) as app_count` + } else { + selectCols = `json_agg(la.app) as lsapps, json_agg(la.localstate) as lsls, json_agg(la.created_at) as ls_created_at, json_agg(la.closed_at) as ls_closed_at, json_agg(la.deleted) as ls_deleted` } + query += `, qls AS (SELECT la.addr, ` + selectCols + ` FROM account_app la JOIN qaccounts ON qaccounts.addr = la.addr` + where + ` GROUP BY 1)` } // query results query += ` SELECT za.addr, za.microalgos, za.rewards_total, za.created_at, za.closed_at, za.deleted, za.rewardsbase, za.keytype, za.account_data` if opts.IncludeAssetHoldings { - query += `, qaa.haid, qaa.hamt, qaa.hf, qaa.holding_created_at, qaa.holding_closed_at, qaa.holding_deleted` + if countOnly { + query += `, qaa.holding_count` + } else { + query += `, qaa.haid, qaa.hamt, qaa.hf, qaa.holding_created_at, qaa.holding_closed_at, qaa.holding_deleted` + } } if opts.IncludeAssetParams { - query += `, qap.paid, qap.pp, qap.asset_created_at, qap.asset_closed_at, qap.asset_deleted` + if countOnly { + query += `, qap.asset_count` + } else { + query += `, qap.paid, qap.pp, qap.asset_created_at, qap.asset_closed_at, qap.asset_deleted` + } } if opts.IncludeAppParams { - query += `, qapp.papps, qapp.ppa, qapp.app_created_at, qapp.app_closed_at, qapp.app_deleted` + if countOnly { + query += `, qapp.app_count` + } else { + query += `, qapp.papps, qapp.ppa, qapp.app_created_at, qapp.app_closed_at, qapp.app_deleted` + } } if opts.IncludeAppLocalState { - query += `, qls.lsapps, qls.lsls, qls.ls_created_at, qls.ls_closed_at, qls.ls_deleted` + if countOnly { + query += `, qls.ls_count` + } else { + query += `, qls.lsapps, qls.lsls, qls.ls_created_at, qls.ls_closed_at, qls.ls_deleted` + } } query += ` FROM qaccounts za`