Skip to content

Commit

Permalink
Unit test of the AccountAssets endpoint. Updated pagination logic to …
Browse files Browse the repository at this point in the history
…query one item extra each time to see if we have actually tokenized through all results before returning. Handler now deals with asset params being optional (in the case of a deleted asset).
  • Loading branch information
gmalouf committed Apr 1, 2024
1 parent ba12ee9 commit 4a3c512
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 12 deletions.
21 changes: 10 additions & 11 deletions daemon/algod/api/server/v2/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,8 @@ func (v2 *Handlers) AccountAssetsInformation(ctx echo.Context, address string, p
// 3. Prepare JSON response
lastRound := ledger.Latest()

records, lookupRound, err := ledger.LookupAssets(lastRound, addr, basics.AssetIndex(assetGreaterThan), *params.Limit)
// We intentionally request one more than the limit to determine if there are more assets.
records, lookupRound, err := ledger.LookupAssets(lastRound, addr, basics.AssetIndex(assetGreaterThan), *params.Limit+1)

if err != nil {
return internalError(ctx, err, errFailedLookingUpLedger, v2.Log)
Expand All @@ -1097,8 +1098,10 @@ func (v2 *Handlers) AccountAssetsInformation(ctx echo.Context, address string, p
// prepare JSON response
response := model.AccountAssetHoldingsResponse{Round: uint64(lookupRound)}

// We always are setting the next token, the client-side can use the total app counts to determine if there are more.
if len(records) > 0 {
// If the total count is greater than the limit, we set the next token to the last asset ID being returned
if uint64(len(records)) > *params.Limit {
// we do not include the last record in the response
records = records[:*params.Limit]
nextTk := strconv.FormatUint(uint64(records[len(records)-1].AssetID), 10)
response.NextToken = &nextTk
}
Expand All @@ -1111,21 +1114,17 @@ func (v2 *Handlers) AccountAssetsInformation(ctx echo.Context, address string, p
continue
}

if record.AssetParams == nil {
v2.Log.Warnf("AccountAssetsInformation: asset %d has no params - should not be possible", record.AssetID)
continue
}

asset := AssetParamsToAsset(record.Creator.String(), record.AssetID, record.AssetParams)

aah := model.AccountAssetHolding{
AssetHolding: model.AssetHolding{
Amount: record.AssetHolding.Amount,
AssetID: uint64(record.AssetID),
IsFrozen: record.AssetHolding.Frozen,
},
}

AssetParams: &asset.Params,
if record.AssetParams != nil {
asset := AssetParamsToAsset(record.Creator.String(), record.AssetID, record.AssetParams)
aah.AssetParams = &asset.Params
}

assetHoldings = append(assetHoldings, aah)
Expand Down
156 changes: 155 additions & 1 deletion daemon/algod/api/server/v2/test/handlers_resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"

"github.com/algorand/go-algorand/data/transactions/logic"
Expand Down Expand Up @@ -109,7 +110,29 @@ func (l *mockLedger) LookupAsset(rnd basics.Round, addr basics.Address, aidx bas
}

func (l *mockLedger) LookupAssets(rnd basics.Round, addr basics.Address, assetIDGT basics.AssetIndex, limit uint64) ([]ledgercore.AssetResourceWithIDs, basics.Round, error) {
panic("implement me")
ad, ok := l.accounts[addr]
if !ok {
return nil, rnd, nil
}

var res []ledgercore.AssetResourceWithIDs
for i := assetIDGT + 1; i < assetIDGT+1+basics.AssetIndex(limit); i++ {
apr := ledgercore.AssetResourceWithIDs{}
if ap, ok := ad.AssetParams[i]; ok {
apr.AssetParams = &ap
apr.Creator = basics.Address{}
}

if ah, ok := ad.Assets[i]; ok {
apr.AssetHolding = &ah
}

if apr.AssetParams != nil || apr.AssetHolding != nil {
apr.AssetID = i
res = append(res, apr)
}
}
return res, rnd, nil
}

func (l *mockLedger) LookupApplication(rnd basics.Round, addr basics.Address, aidx basics.AppIndex) (ar ledgercore.AppResource, err error) {
Expand Down Expand Up @@ -216,6 +239,23 @@ func randomAccountWithAssetParams(N int) basics.AccountData {
return a
}

func randomAccountWithSomeAssetHoldingsAndOverlappingAssetParams(overlapN int, nonOverlapAssetHoldingsN int) basics.AccountData {
a := ledgertesting.RandomAccountData(0)
a.AssetParams = make(map[basics.AssetIndex]basics.AssetParams)
a.Assets = make(map[basics.AssetIndex]basics.AssetHolding)
// overlapN assets have both asset params and asset holdings
for i := 1; i <= overlapN; i++ {
a.AssetParams[basics.AssetIndex(i)] = ledgertesting.RandomAssetParams()
a.Assets[basics.AssetIndex(i)] = ledgertesting.RandomAssetHolding(false)
}

// nonOverlapAssetHoldingsN assets have only asset holdings
for i := overlapN + 1; i <= (overlapN + nonOverlapAssetHoldingsN); i++ {
a.Assets[basics.AssetIndex(i)] = ledgertesting.RandomAssetHolding(false)
}
return a
}

func randomAccountWithAppLocalState(N int) basics.AccountData {
a := ledgertesting.RandomAccountData(0)
a.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState)
Expand Down Expand Up @@ -246,6 +286,7 @@ func setupTestForLargeResources(t *testing.T, acctSize, maxResults int, accountM

mockNode := makeMockNode(&ml, t.Name(), nil, cannedStatusReportGolden, false)
mockNode.config.MaxAPIResourcesPerAccount = uint64(maxResults)
mockNode.config.EnableExperimentalAPI = true
dummyShutdownChan := make(chan struct{})
handlers = v2.Handlers{
Node: mockNode,
Expand Down Expand Up @@ -385,6 +426,119 @@ func accountInformationResourceLimitsTest(t *testing.T, accountMaker func(int) b
}
}

func accountAssetInformationResourceLimitsTest(t *testing.T, handlers v2.Handlers, addr basics.Address,
acctData basics.AccountData, params model.AccountAssetsInformationParams, inputNextToken int, maxResults int, expectToken bool) {
fakeLatestRound := basics.Round(10)

ctx, rec := newReq(t)
err := handlers.AccountAssetsInformation(ctx, addr.String(), params)
require.NoError(t, err)
require.Equal(t, 200, rec.Code)
var ret model.AccountAssetHoldingsResponse
err = json.Unmarshal(rec.Body.Bytes(), &ret)
require.NoError(t, err)
assert.Equal(t, fakeLatestRound, basics.Round(ret.Round))

if expectToken {
nextRaw, err0 := strconv.ParseUint(*ret.NextToken, 10, 64)
require.NoError(t, err0)
// The next token decoded is actually the last asset id returned
assert.Equal(t, (*ret.AssetHoldings)[maxResults-1].AssetHolding.AssetID, nextRaw)
}
assert.Equal(t, maxResults, len(*ret.AssetHoldings))

// Asset holdings should match the first limit assets from the account data
minForResults := 0
if inputNextToken > 0 {
minForResults = inputNextToken
}
for i := minForResults; i < minForResults+maxResults; i++ {
expectedIndex := i + 1

assert.Equal(t, acctData.Assets[basics.AssetIndex(expectedIndex)].Amount, (*ret.AssetHoldings)[i-minForResults].AssetHolding.Amount)
assert.Equal(t, acctData.Assets[basics.AssetIndex(expectedIndex)].Frozen, (*ret.AssetHoldings)[i-minForResults].AssetHolding.IsFrozen)
assert.Equal(t, uint64(expectedIndex), (*ret.AssetHoldings)[i-minForResults].AssetHolding.AssetID)
}
}

// TestAccountAssetsInformation tests the account asset information endpoint
func TestAccountAssetsInformation(t *testing.T) {
partitiontest.PartitionTest(t)

accountOverlappingAssetParamsHoldingsCount := 1000
accountNonOverlappingAssetHoldingsCount := 25
totalAssetHoldings := accountOverlappingAssetParamsHoldingsCount + accountNonOverlappingAssetHoldingsCount

handlers, addr, acctData := setupTestForLargeResources(t, accountOverlappingAssetParamsHoldingsCount, 50, func(N int) basics.AccountData {
return randomAccountWithSomeAssetHoldingsAndOverlappingAssetParams(N, accountNonOverlappingAssetHoldingsCount)
})

// 1. Query with no limit/pagination - should get DefaultAssetResults back
accountAssetInformationResourceLimitsTest(t, handlers, addr, acctData, model.AccountAssetsInformationParams{},
0, int(v2.DefaultAssetResults), false)

// 2. Query with limit<total resources, no next - should get the first (lowest asset id to highest) limit results back
rawLimit := 100
limit := uint64(rawLimit)
accountAssetInformationResourceLimitsTest(t, handlers, addr, acctData,
model.AccountAssetsInformationParams{Limit: &limit}, 0, rawLimit, true)

// 3. Query with limit, next
rawNext := 100
nextTk := strconv.FormatUint(uint64(rawNext), 10)
accountAssetInformationResourceLimitsTest(t, handlers, addr, acctData,
model.AccountAssetsInformationParams{Limit: &limit, Next: &nextTk}, rawNext, rawLimit, true)

//4. Query with limit, next to retrieve final batch
rawNext = 1019
nextTk = strconv.FormatUint(uint64(rawNext), 10)
accountAssetInformationResourceLimitsTest(t, handlers, addr, acctData,
model.AccountAssetsInformationParams{Limit: &limit, Next: &nextTk}, rawNext, totalAssetHoldings-rawNext, false)

// 5. Query with limit, next to provide batch, but no data in that range
rawNext = 1025
nextTk = strconv.FormatUint(uint64(rawNext), 10)
accountAssetInformationResourceLimitsTest(t, handlers, addr, acctData,
model.AccountAssetsInformationParams{Limit: &limit, Next: &nextTk}, rawNext, totalAssetHoldings-rawNext, false)

// 6. Malformed address
ctx, rec := newReq(t)
err := handlers.AccountAssetsInformation(ctx, "", model.AccountAssetsInformationParams{})
require.NoError(t, err)
require.Equal(t, 400, rec.Code)
require.Equal(t, "{\"message\":\"failed to parse the address\"}\n", rec.Body.String())

// 7. Unknown address (200 returned, just no asset data)
unknownAddress := basics.Address{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
accountAssetInformationResourceLimitsTest(t, handlers, unknownAddress, basics.AccountData{}, model.AccountAssetsInformationParams{},
0, 0, false)

// 8a. Invalid limits - larger than configured max
ctx, rec = newReq(t)
err = handlers.AccountAssetsInformation(ctx, addr.String(), model.AccountAssetsInformationParams{
Limit: func() *uint64 {
l := uint64(v2.MaxAssetResults + 1)
return &l
}(),
})
require.NoError(t, err)
require.Equal(t, 400, rec.Code)
require.Equal(t, "{\"message\":\"limit 10001 exceeds max assets single batch limit 10000\"}\n", rec.Body.String())

// 8b. Invalid limits - zero
ctx, rec = newReq(t)
err = handlers.AccountAssetsInformation(ctx, addr.String(), model.AccountAssetsInformationParams{
Limit: func() *uint64 {
l := uint64(0)
return &l
}(),
})
require.NoError(t, err)
require.Equal(t, 400, rec.Code)
require.Equal(t, "{\"message\":\"limit parameter must be a positive integer\"}\n", rec.Body.String())

}

func TestAccountInformationResourceLimits(t *testing.T) {
partitiontest.PartitionTest(t)

Expand Down

0 comments on commit 4a3c512

Please sign in to comment.