diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index 9c1bad6786..46f7534c31 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -308,7 +308,7 @@ func (v2 *Handlers) AccountInformation(ctx echo.Context, address string, params Message: "Result limit exceeded", MaxResults: &maxResults, TotalAssetsOptedIn: &record.TotalAssets, - TotalCreatedAssets: &record.TotalAppLocalStates, + TotalCreatedAssets: &record.TotalAssetParams, TotalAppsOptedIn: &record.TotalAppLocalStates, TotalCreatedApps: &record.TotalAppParams, }) diff --git a/daemon/algod/api/server/v2/test/handlers_resources_test.go b/daemon/algod/api/server/v2/test/handlers_resources_test.go index c8e0134f52..ba2ffbaf3d 100644 --- a/daemon/algod/api/server/v2/test/handlers_resources_test.go +++ b/daemon/algod/api/server/v2/test/handlers_resources_test.go @@ -34,6 +34,7 @@ import ( "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -64,7 +65,26 @@ func (l *mockLedger) ConsensusParams(r basics.Round) (config.ConsensusParams, er func (l *mockLedger) Latest() basics.Round { return l.latest } func (l *mockLedger) LookupResource(rnd basics.Round, addr basics.Address, aidx basics.CreatableIndex, ctype basics.CreatableType) (ar ledgercore.AccountResource, err error) { - panic("not implemented") + ad, ok := l.accounts[addr] + if !ok { + return ledgercore.AccountResource{}, nil + } + if ctype == basics.AppCreatable { + if ap, ok := ad.AppParams[basics.AppIndex(aidx)]; ok { + ar.AppParams = &ap + } + if ls, ok := ad.AppLocalStates[basics.AppIndex(aidx)]; ok { + ar.AppLocalState = &ls + } + } else { + if ap, ok := ad.AssetParams[basics.AssetIndex(aidx)]; ok { + ar.AssetParams = &ap + } + if ah, ok := ad.Assets[basics.AssetIndex(aidx)]; ok { + ar.AssetHolding = &ah + } + } + return ar, nil } func (l *mockLedger) BlockCert(rnd basics.Round) (blk bookkeeping.Block, cert agreement.Certificate, err error) { panic("not implemented") @@ -109,14 +129,51 @@ func randomAccountWithResources(N int) basics.AccountData { return a } -func setupTestForLargeResources(t *testing.T, acctSize, maxResults int) (handlers v2.Handlers, ctx echo.Context, rec *httptest.ResponseRecorder, fakeAddr basics.Address) { +func randomAccountWithAssets(N int) basics.AccountData { + a := ledgertesting.RandomAccountData(0) + a.Assets = make(map[basics.AssetIndex]basics.AssetHolding) + for i := 0; i < N; i++ { + a.Assets[basics.AssetIndex(i*4)] = ledgertesting.RandomAssetHolding(false) + } + return a +} + +func randomAccountWithAssetParams(N int) basics.AccountData { + a := ledgertesting.RandomAccountData(0) + a.AssetParams = make(map[basics.AssetIndex]basics.AssetParams) + for i := 0; i < N; i++ { + a.AssetParams[basics.AssetIndex(i*4+1)] = ledgertesting.RandomAssetParams() + } + return a +} + +func randomAccountWithAppLocalState(N int) basics.AccountData { + a := ledgertesting.RandomAccountData(0) + a.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState) + for i := 0; i < N; i++ { + a.AppLocalStates[basics.AppIndex(i*4+2)] = ledgertesting.RandomAppLocalState() + } + return a +} + +func randomAccountWithAppParams(N int) basics.AccountData { + a := ledgertesting.RandomAccountData(0) + a.AppParams = make(map[basics.AppIndex]basics.AppParams) + for i := 0; i < N; i++ { + a.AppParams[basics.AppIndex(i*4+3)] = ledgertesting.RandomAppParams() + } + return a +} + +func setupTestForLargeResources(t *testing.T, acctSize, maxResults int, accountMaker func(int) basics.AccountData) (handlers v2.Handlers, fakeAddr basics.Address, acctData basics.AccountData) { ml := mockLedger{ accounts: make(map[basics.Address]basics.AccountData), latest: basics.Round(10), } fakeAddr = ledgertesting.RandomAddress() - ml.accounts[fakeAddr] = randomAccountWithResources(acctSize) + acctData = accountMaker(acctSize) + ml.accounts[fakeAddr] = acctData mockNode := makeMockNode(&ml, t.Name(), nil) mockNode.config.MaxAPIResourcesPerAccount = uint64(maxResults) @@ -126,56 +183,145 @@ func setupTestForLargeResources(t *testing.T, acctSize, maxResults int) (handler Log: logging.Base(), Shutdown: dummyShutdownChan, } + return +} + +func newReq(t *testing.T) (ctx echo.Context, rec *httptest.ResponseRecorder) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec = httptest.NewRecorder() ctx = e.NewContext(req, rec) - return } -func accountInformationResourceLimitsTest(t *testing.T, acctSize, maxResults int, exclude string, expectedCode int) { - handlers, ctx, rec, addr := setupTestForLargeResources(t, acctSize, maxResults) +func accountInformationResourceLimitsTest(t *testing.T, accountMaker func(int) basics.AccountData, acctSize, maxResults int, exclude string, expectedCode int) { + handlers, addr, acctData := setupTestForLargeResources(t, acctSize, maxResults, accountMaker) params := generatedV2.AccountInformationParams{} if exclude != "" { params.Exclude = &exclude } + ctx, rec := newReq(t) err := handlers.AccountInformation(ctx, addr.String(), params) require.NoError(t, err) require.Equal(t, expectedCode, rec.Code) - var ret map[string]interface{} + var ret struct { + TotalApps int `json:"total-apps-opted-in"` + TotalAssets int `json:"total-assets-opted-in"` + TotalCreatedApps int `json:"total-created-apps"` + TotalCreatedAssets int `json:"total-created-assets"` + MaxResults int `json:"max-results"` + Apps []interface{} `json:"apps-local-state"` + Assets []interface{} `json:"assets"` + CreatedApps []interface{} `json:"createdApps"` + CreatedAssets []interface{} `json:"createdAssets"` + } + err = json.Unmarshal(rec.Body.Bytes(), &ret) require.NoError(t, err) // totals should be present in both 200 and 400 - require.Equal(t, float64(acctSize), ret["total-apps-opted-in"].(float64)+ret["total-assets-opted-in"].(float64)+ret["total-created-apps"].(float64)+ret["total-created-assets"].(float64)) + require.Equal(t, acctSize, ret.TotalApps+ret.TotalAssets+ret.TotalCreatedApps+ret.TotalCreatedAssets, "totals incorrect: %+v", ret) // check totals switch rec.Code { case 400: - require.Equal(t, float64(maxResults), ret["max-results"]) + require.Equal(t, maxResults, ret.MaxResults) case 200: if exclude == "all" { - require.Nil(t, ret["apps-local-state"]) - require.Nil(t, ret["assets"]) - require.Nil(t, ret["created-apps"]) - require.Nil(t, ret["created-assets"]) + require.Nil(t, ret.Apps) + require.Nil(t, ret.Assets) + require.Nil(t, ret.CreatedApps) + require.Nil(t, ret.CreatedAssets) } else if exclude == "none" { - require.Equal(t, acctSize, len(ret["apps-local-state"].([]interface{}))+ - len(ret["assets"].([]interface{}))+ - len(ret["created-apps"].([]interface{}))+ - len(ret["created-assets"].([]interface{}))) + require.Equal(t, acctSize, len(ret.Apps)+len(ret.Assets)+len(ret.CreatedApps)+len(ret.CreatedAssets)) } } + + // check individual assets/apps + for i := 0; i < ret.TotalAssets; i++ { + ctx, rec = newReq(t) + aidx := basics.AssetIndex(i * 4) + err = handlers.AccountAssetInformation(ctx, addr.String(), uint64(aidx), generatedV2.AccountAssetInformationParams{}) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) + var ret generatedV2.AccountAssetResponse + err = json.Unmarshal(rec.Body.Bytes(), &ret) + require.NoError(t, err) + assert.Nil(t, ret.CreatedAsset) + assert.Equal(t, ret.AssetHolding, &generatedV2.AssetHolding{ + Amount: acctData.Assets[aidx].Amount, + AssetId: uint64(aidx), + IsFrozen: acctData.Assets[aidx].Frozen, + }) + } + for i := 0; i < ret.TotalCreatedAssets; i++ { + ctx, rec = newReq(t) + aidx := basics.AssetIndex(i*4 + 1) + err = handlers.AccountAssetInformation(ctx, addr.String(), uint64(aidx), generatedV2.AccountAssetInformationParams{}) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) + var ret generatedV2.AccountAssetResponse + err = json.Unmarshal(rec.Body.Bytes(), &ret) + require.NoError(t, err) + assert.Nil(t, ret.AssetHolding) + ap := acctData.AssetParams[aidx] + assetParams := v2.AssetParamsToAsset(addr.String(), aidx, &ap) + assert.Equal(t, ret.CreatedAsset, &assetParams.Params) + } + for i := 0; i < ret.TotalApps; i++ { + ctx, rec = newReq(t) + aidx := basics.AppIndex(i*4 + 2) + err = handlers.AccountApplicationInformation(ctx, addr.String(), uint64(aidx), generatedV2.AccountApplicationInformationParams{}) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) + var ret generatedV2.AccountApplicationResponse + err = json.Unmarshal(rec.Body.Bytes(), &ret) + require.NoError(t, err) + assert.Nil(t, ret.CreatedApp) + require.NotNil(t, ret.AppLocalState) + assert.Equal(t, uint64(aidx), ret.AppLocalState.Id) + ls := acctData.AppLocalStates[aidx] + assert.Equal(t, ls.Schema.NumByteSlice, ret.AppLocalState.Schema.NumByteSlice) + assert.Equal(t, ls.Schema.NumUint, ret.AppLocalState.Schema.NumUint) + } + for i := 0; i < ret.TotalCreatedApps; i++ { + ctx, rec = newReq(t) + aidx := basics.AppIndex(i*4 + 3) + err = handlers.AccountApplicationInformation(ctx, addr.String(), uint64(aidx), generatedV2.AccountApplicationInformationParams{}) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) + var ret generatedV2.AccountApplicationResponse + err = json.Unmarshal(rec.Body.Bytes(), &ret) + require.NoError(t, err) + assert.Nil(t, ret.AppLocalState) + ap := acctData.AppParams[aidx] + expAp := v2.AppParamsToApplication(addr.String(), aidx, &ap) + assert.EqualValues(t, expAp.Params.ApprovalProgram, ret.CreatedApp.ApprovalProgram) + assert.EqualValues(t, expAp.Params.ClearStateProgram, ret.CreatedApp.ClearStateProgram) + assert.EqualValues(t, expAp.Params.Creator, ret.CreatedApp.Creator) + } } func TestAccountInformationResourceLimits(t *testing.T) { partitiontest.PartitionTest(t) - accountInformationResourceLimitsTest(t, 99, 100, "", 200) // under limit - accountInformationResourceLimitsTest(t, 101, 100, "", 400) // over limit - accountInformationResourceLimitsTest(t, 100, 100, "", 200) // at limit - accountInformationResourceLimitsTest(t, 101, 100, "all", 200) // over limit with exclude=all - accountInformationResourceLimitsTest(t, 101, 100, "none", 400) // over limit with exclude=none + for _, tc := range []struct { + name string + accountMaker func(int) basics.AccountData + }{ + {name: "all", accountMaker: randomAccountWithResources}, + {name: "assets", accountMaker: randomAccountWithAssets}, + {name: "applocal", accountMaker: randomAccountWithAppLocalState}, + {name: "assetparams", accountMaker: randomAccountWithAssetParams}, + {name: "appparams", accountMaker: randomAccountWithAppParams}, + } { + t.Run(tc.name, func(t *testing.T) { + accountInformationResourceLimitsTest(t, tc.accountMaker, 99, 100, "", 200) // under limit + accountInformationResourceLimitsTest(t, tc.accountMaker, 101, 100, "", 400) // over limit + accountInformationResourceLimitsTest(t, tc.accountMaker, 100, 100, "", 200) // at limit + accountInformationResourceLimitsTest(t, tc.accountMaker, 101, 100, "all", 200) // over limit with exclude=all + accountInformationResourceLimitsTest(t, tc.accountMaker, 101, 100, "none", 400) // over limit with exclude=none + }) + } }