From a99e6f7b6c43852704e28258fb505ba5e5116304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Godnic?= Date: Fri, 20 Dec 2024 16:32:13 -0300 Subject: [PATCH] Implement the endpoint `GET /v2/block-headers` (#1638) * Implement `GET /v2/blocks` Currently using the exact same implementation as https://github.com/AlgoNode/indexer-api-cdb/pull/10 * Rename function * Rename function * Fix compiler error * Revert f17423ba2ad117cd23ff7d9eba49a7c5cc9ee3ee * Update file generated by mockery * Fix typo * Improve parameter descriptions * Change route to `GET /v2/block-headers` * Remove the `updates` and `participation` params Remove the `updates` and `participation` parameters from `GET /v2/block-headers`. The underlying SQL code is now simpler. * Lints * Rename struct * Fix outdated comment * Use faster/simpler sorting function * Use a more descriptive name for func `rowToBlock` * Remove decodeAddress / decodeAddressToBytes Remove the functions `decodeAddress` and `decodeAddressToBytes`. Also, add more context information to the errors being returned. * Attempt at fixing broken test Attempt at fixing `TestTimeouts/LookupAccountTransactions` * Change function `hdrRowToBlock` signature Change function `hdrRowToBlock` signature to be in line with other similar functions. * Rename `proposer` parameter to `proposers` In `GET /v2/block-headers`, rename the `proposer` parameter to `proposers` to follow conventions through the rest of the API. --- api/converter_utils.go | 244 +++++++++++++-- api/error_messages.go | 2 + api/generated/common/routes.go | 368 +++++++++++------------ api/generated/common/types.go | 20 ++ api/generated/v2/routes.go | 529 +++++++++++++++++++-------------- api/generated/v2/types.go | 50 ++++ api/handlers.go | 127 +++++++- api/handlers_test.go | 2 +- api/indexer.oas2.json | 130 +++++++- api/indexer.oas3.yml | 255 ++++++++++++++++ api/server.go | 10 + cmd/algorand-indexer/daemon.go | 9 + idb/dummy/dummy.go | 5 + idb/idb.go | 45 +++ idb/mocks/IndexerDb.go | 30 ++ idb/postgres/postgres.go | 179 +++++++++++ 16 files changed, 1554 insertions(+), 451 deletions(-) diff --git a/api/converter_utils.go b/api/converter_utils.go index 8b4081acd..8a854c231 100644 --- a/api/converter_utils.go +++ b/api/converter_utils.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "errors" "fmt" + "slices" "sort" "strconv" "strings" @@ -34,19 +35,6 @@ func decodeDigest(str *string, field string, errorArr []string) (string, []strin return "", errorArr } -// decodeAddress returns the byte representation of the input string, or appends an error to errorArr -func decodeAddress(str *string, field string, errorArr []string) ([]byte, []string) { - if str != nil { - addr, err := sdk.DecodeAddress(*str) - if err != nil { - return nil, append(errorArr, fmt.Sprintf("%s '%s': %v", errUnableToParseAddress, field, err)) - } - return addr[:], errorArr - } - // Pass through - return nil, errorArr -} - // decodeAddress converts the role information into a bitmask, or appends an error to errorArr func decodeAddressRole(role *string, excludeCloseTo *bool, errorArr []string) (idb.AddressRole, []string) { // If the string is nil, return early. @@ -298,6 +286,94 @@ func txnRowToTransaction(row idb.TxnRow) (generated.Transaction, error) { return txn, nil } +func hdrRowToBlock(row idb.BlockRow) generated.Block { + + rewards := generated.BlockRewards{ + FeeSink: row.BlockHeader.FeeSink.String(), + RewardsCalculationRound: uint64(row.BlockHeader.RewardsRecalculationRound), + RewardsLevel: row.BlockHeader.RewardsLevel, + RewardsPool: row.BlockHeader.RewardsPool.String(), + RewardsRate: row.BlockHeader.RewardsRate, + RewardsResidue: row.BlockHeader.RewardsResidue, + } + + upgradeState := generated.BlockUpgradeState{ + CurrentProtocol: string(row.BlockHeader.CurrentProtocol), + NextProtocol: strPtr(string(row.BlockHeader.NextProtocol)), + NextProtocolApprovals: uint64Ptr(row.BlockHeader.NextProtocolApprovals), + NextProtocolSwitchOn: uint64Ptr(uint64(row.BlockHeader.NextProtocolSwitchOn)), + NextProtocolVoteBefore: uint64Ptr(uint64(row.BlockHeader.NextProtocolVoteBefore)), + } + + upgradeVote := generated.BlockUpgradeVote{ + UpgradeApprove: boolPtr(row.BlockHeader.UpgradeApprove), + UpgradeDelay: uint64Ptr(uint64(row.BlockHeader.UpgradeDelay)), + UpgradePropose: strPtr(string(row.BlockHeader.UpgradePropose)), + } + + var partUpdates *generated.ParticipationUpdates = &generated.ParticipationUpdates{} + if len(row.BlockHeader.ExpiredParticipationAccounts) > 0 { + addrs := make([]string, len(row.BlockHeader.ExpiredParticipationAccounts)) + for i := 0; i < len(addrs); i++ { + addrs[i] = row.BlockHeader.ExpiredParticipationAccounts[i].String() + } + partUpdates.ExpiredParticipationAccounts = strArrayPtr(addrs) + } + if len(row.BlockHeader.AbsentParticipationAccounts) > 0 { + addrs := make([]string, len(row.BlockHeader.AbsentParticipationAccounts)) + for i := 0; i < len(addrs); i++ { + addrs[i] = row.BlockHeader.AbsentParticipationAccounts[i].String() + } + partUpdates.AbsentParticipationAccounts = strArrayPtr(addrs) + } + if *partUpdates == (generated.ParticipationUpdates{}) { + partUpdates = nil + } + + // order these so they're deterministic + orderedTrackingTypes := make([]sdk.StateProofType, len(row.BlockHeader.StateProofTracking)) + trackingArray := make([]generated.StateProofTracking, len(row.BlockHeader.StateProofTracking)) + elems := 0 + for key := range row.BlockHeader.StateProofTracking { + orderedTrackingTypes[elems] = key + elems++ + } + slices.Sort(orderedTrackingTypes) + for i := 0; i < len(orderedTrackingTypes); i++ { + stpfTracking := row.BlockHeader.StateProofTracking[orderedTrackingTypes[i]] + thing1 := generated.StateProofTracking{ + NextRound: uint64Ptr(uint64(stpfTracking.StateProofNextRound)), + Type: uint64Ptr(uint64(orderedTrackingTypes[i])), + VotersCommitment: byteSliceOmitZeroPtr(stpfTracking.StateProofVotersCommitment), + OnlineTotalWeight: uint64Ptr(uint64(stpfTracking.StateProofOnlineTotalWeight)), + } + trackingArray[orderedTrackingTypes[i]] = thing1 + } + + ret := generated.Block{ + Bonus: uint64PtrOrNil(uint64(row.BlockHeader.Bonus)), + FeesCollected: uint64PtrOrNil(uint64(row.BlockHeader.FeesCollected)), + GenesisHash: row.BlockHeader.GenesisHash[:], + GenesisId: row.BlockHeader.GenesisID, + ParticipationUpdates: partUpdates, + PreviousBlockHash: row.BlockHeader.Branch[:], + Proposer: addrPtr(row.BlockHeader.Proposer), + ProposerPayout: uint64PtrOrNil(uint64(row.BlockHeader.ProposerPayout)), + Rewards: &rewards, + Round: uint64(row.BlockHeader.Round), + Seed: row.BlockHeader.Seed[:], + StateProofTracking: &trackingArray, + Timestamp: uint64(row.BlockHeader.TimeStamp), + Transactions: nil, + TransactionsRoot: row.BlockHeader.TxnCommitments.NativeSha512_256Commitment[:], + TransactionsRootSha256: row.BlockHeader.TxnCommitments.Sha256Commitment[:], + TxnCounter: uint64Ptr(row.BlockHeader.TxnCounter), + UpgradeState: &upgradeState, + UpgradeVote: &upgradeVote, + } + return ret +} + func signedTxnWithAdToTransaction(stxn *sdk.SignedTxnWithAD, extra rowData) (generated.Transaction, error) { var payment *generated.TransactionPayment var keyreg *generated.TransactionKeyreg @@ -640,9 +716,14 @@ func edIndexToAddress(index uint64, txn sdk.Transaction, shared []sdk.Address) ( } func (si *ServerImplementation) assetParamsToAssetQuery(params generated.SearchForAssetsParams) (idb.AssetsQuery, error) { - creator, errorArr := decodeAddress(params.Creator, "creator", make([]string, 0)) - if len(errorArr) != 0 { - return idb.AssetsQuery{}, errors.New(errUnableToParseAddress) + + var creatorAddressBytes []byte + if params.Creator != nil { + creator, err := sdk.DecodeAddress(*params.Creator) + if err != nil { + return idb.AssetsQuery{}, fmt.Errorf("unable to parse creator address: %w", err) + } + creatorAddressBytes = creator[:] } var assetGreaterThan *uint64 @@ -657,7 +738,7 @@ func (si *ServerImplementation) assetParamsToAssetQuery(params generated.SearchF query := idb.AssetsQuery{ AssetID: params.AssetId, AssetIDGreaterThan: assetGreaterThan, - Creator: creator, + Creator: creatorAddressBytes, Name: strOrDefault(params.Name), Unit: strOrDefault(params.Unit), Query: "", @@ -669,9 +750,14 @@ func (si *ServerImplementation) assetParamsToAssetQuery(params generated.SearchF } func (si *ServerImplementation) appParamsToApplicationQuery(params generated.SearchForApplicationsParams) (idb.ApplicationQuery, error) { - addr, errorArr := decodeAddress(params.Creator, "creator", make([]string, 0)) - if len(errorArr) != 0 { - return idb.ApplicationQuery{}, errors.New(errUnableToParseAddress) + + var creatorAddressBytes []byte + if params.Creator != nil { + addr, err := sdk.DecodeAddress(*params.Creator) + if err != nil { + return idb.ApplicationQuery{}, fmt.Errorf("unable to parse creator address: %w", err) + } + creatorAddressBytes = addr[:] } var appGreaterThan *uint64 @@ -686,7 +772,7 @@ func (si *ServerImplementation) appParamsToApplicationQuery(params generated.Sea return idb.ApplicationQuery{ ApplicationID: params.ApplicationId, ApplicationIDGreaterThan: appGreaterThan, - Address: addr, + Address: creatorAddressBytes, IncludeDeleted: boolOrDefault(params.IncludeAll), Limit: min(uintOrDefaultValue(params.Limit, si.opts.DefaultApplicationsLimit), si.opts.MaxApplicationsLimit), }, nil @@ -708,7 +794,15 @@ func (si *ServerImplementation) transactionParamsToTransactionFilter(params gene filter.NextToken = strOrDefault(params.Next) // Address - filter.Address, errorArr = decodeAddress(params.Address, "address", errorArr) + if params.Address != nil { + addr, err := sdk.DecodeAddress(*params.Address) + if err != nil { + errorArr = append(errorArr, fmt.Sprintf("%s: %v", errUnableToParseAddress, err)) + } + filter.Address = addr[:] + } + + // Txid filter.Txid, errorArr = decodeDigest(params.Txid, "txid", errorArr) // Byte array @@ -749,6 +843,112 @@ func (si *ServerImplementation) transactionParamsToTransactionFilter(params gene return } +func (si *ServerImplementation) blockParamsToBlockFilter(params generated.SearchForBlockHeadersParams) (filter idb.BlockHeaderFilter, err error) { + + var errs []error + + // Integer + filter.Limit = min(uintOrDefaultValue(params.Limit, si.opts.DefaultBlocksLimit), si.opts.MaxBlocksLimit) + // If min/max are mixed up + // + // This check is performed here instead of in validateBlockFilter because + // when converting params into a filter, the next token is merged with params.MinRound. + if params.MinRound != nil && params.MaxRound != nil && *params.MinRound > *params.MaxRound { + errs = append(errs, errors.New(errInvalidRoundMinMax)) + } + filter.MaxRound = params.MaxRound + filter.MinRound = params.MinRound + + // String + if params.Next != nil { + n, err := idb.DecodeBlockRowNext(*params.Next) + if err != nil { + errs = append(errs, fmt.Errorf("%s: %w", errUnableToParseNext, err)) + } + // Set the MinRound + if filter.MinRound == nil { + filter.MinRound = uint64Ptr(n + 1) + } else { + filter.MinRound = uint64Ptr(max(*filter.MinRound, n+1)) + } + } + + // Time + if params.AfterTime != nil { + filter.AfterTime = *params.AfterTime + } + if params.BeforeTime != nil { + filter.BeforeTime = *params.BeforeTime + } + + // Address list + { + // Make sure at most one of the participation parameters is set + numParticipationFilters := 0 + if params.Proposers != nil { + numParticipationFilters++ + } + if params.Expired != nil { + numParticipationFilters++ + } + if params.Absent != nil { + numParticipationFilters++ + } + if numParticipationFilters > 1 { + errs = append(errs, errors.New("only one of `proposer`, `expired`, or `absent` can be specified")) + } + + // Validate the number of items in the participation account lists + if params.Proposers != nil && uint64(len(*params.Proposers)) > si.opts.MaxAccountListSize { + errs = append(errs, fmt.Errorf("proposers list too long, max size is %d", si.opts.MaxAccountListSize)) + } + if params.Expired != nil && uint64(len(*params.Expired)) > si.opts.MaxAccountListSize { + errs = append(errs, fmt.Errorf("expired list too long, max size is %d", si.opts.MaxAccountListSize)) + } + if params.Absent != nil && uint64(len(*params.Absent)) > si.opts.MaxAccountListSize { + errs = append(errs, fmt.Errorf("absent list too long, max size is %d", si.opts.MaxAccountListSize)) + } + + filter.Proposers = make(map[sdk.Address]struct{}, 0) + if params.Proposers != nil { + for _, s := range *params.Proposers { + addr, err := sdk.DecodeAddress(s) + if err != nil { + errs = append(errs, fmt.Errorf("unable to parse proposer address `%s`: %w", s, err)) + } else { + filter.Proposers[addr] = struct{}{} + } + } + } + + filter.ExpiredParticipationAccounts = make(map[sdk.Address]struct{}, 0) + if params.Expired != nil { + for _, s := range *params.Expired { + addr, err := sdk.DecodeAddress(s) + if err != nil { + errs = append(errs, fmt.Errorf("unable to parse expired address `%s`: %w", s, err)) + } else { + filter.ExpiredParticipationAccounts[addr] = struct{}{} + } + } + } + + filter.AbsentParticipationAccounts = make(map[sdk.Address]struct{}, 0) + if params.Absent != nil { + for _, s := range *params.Absent { + addr, err := sdk.DecodeAddress(s) + if err != nil { + errs = append(errs, fmt.Errorf("unable to parse absent address `%s`: %w", s, err)) + } else { + filter.AbsentParticipationAccounts[addr] = struct{}{} + } + } + } + } + + return filter, errors.Join(errs...) +} + func (si *ServerImplementation) maxAccountsErrorToAccountsErrorResponse(maxErr idb.MaxAPIResourcesPerAccountError) generated.ErrorResponse { addr := maxErr.Address.String() max := uint64(si.opts.MaxAPIResourcesPerAccount) diff --git a/api/error_messages.go b/api/error_messages.go index a1bfbfbc2..cd364ab46 100644 --- a/api/error_messages.go +++ b/api/error_messages.go @@ -11,6 +11,7 @@ import ( const ( errInvalidRoundAndMinMax = "cannot specify round and min-round/max-round" errInvalidRoundMinMax = "min-round must be less than max-round" + errInvalidTimeMinMax = "after-time must be less than before-time" errUnableToParseAddress = "unable to parse address" errInvalidCreatorAddress = "found an invalid creator address" errUnableToParseBase64 = "unable to parse base64 data" @@ -38,6 +39,7 @@ const ( ErrFailedLookingUpBoxes = "failed while looking up application boxes" errRewindingAccountNotSupported = "rewinding account is no longer supported, please remove the `round=` query parameter and try again" errLookingUpBlockForRound = "error while looking up block for round" + errBlockHeaderSearch = "error while searching for block headers" errTransactionSearch = "error while searching for transaction" errZeroAddressCloseRemainderToRole = "searching transactions by zero address with close address role is not supported" errZeroAddressAssetSenderRole = "searching transactions by zero address with asset sender role is not supported" diff --git a/api/generated/common/routes.go b/api/generated/common/routes.go index bf1f44d7f..e70663e5b 100644 --- a/api/generated/common/routes.go +++ b/api/generated/common/routes.go @@ -72,189 +72,191 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "+ge3+k7gAAA=", + "H4sIAAAAAAAC/+x9f5PbNrLgV0HpXlXsnDjjOJvU26naeuXYccW1dtZlO9m758ndQmRLQoYCGAAcScn5", + "u1+hGyBBEpSomfHYW5W/7BHxo4FuNBr9849ZrjaVkiCtmV38Mau45huwoPEvvjAgrftfASbXorJCydnF", + "7Emeq1pawzZcX0HBuGHUlAnJ7BrYolT5FVsDL0B/YVjFtRW5qLjrz+qq4BbMGXu3FviNZmQ8z6GyhnGW", + "q82GMwPum4WClcJYppaMF4UGY8CczeYz2FWlKmB2seSlgflMOMh+q0HvZ/OZ5BuYXYQFzGcmX8OGu5UI", + "CxtcnN1XromxWsjVbD7bZbxcKc1lkS2V3nDrFkoTzj7MQ3OuNd+7v43dl+4H19b9zWlPMlEM98t/Y81c", + "CGvF7ToCte0/n2n4rRYaitmF1TXE4Heh/uAm9jAOZv2HLPdMyLysC2BWc2l47j4ZthV2zazbfd/Z4U1J", + "cHvs0Bc1ZksBZYEbntxgP/k4iEc39shnP0Omldvu/hqfqs1CSAgrgmZBLVlZxQpYYqM1t8xBF9GS+2yA", + "63zNlkofWSYBEa8VZL2ZXbyfGZAFaMRcDuIa/7vUAL9DZrlegZ39Mk/hbmlBZ1ZsEkt74TGnwdSlOxZL", + "XM0a2Epcg2Su1xl7VRvLFsC4ZG+eP2Vff/31Xxltozs4NNXoqtrZ4zU1WHDHNHyegtQ3z5/i/G/9Aqe2", + "4lVVihyZQ/L4PGm/sxfPxhbTHSRBkEJaWIGmjTcG0mf1iftyYJrQ8dgEtV1njmzGEcsDF82VXIpVraFw", + "1FgboLNpKpCFkCt2BftRFDbTfLwTuICl0jCRSqnxnZJpPP8npdOF2mUE04Bo2ELtmPvmOOlK8TLjeoUr", + "ZF+AzJXD48U1L2v44ow9V5oJac3c4xp8QyHtxVePv/6Lb6L5li32FgbtFt/+5eLJ3/7mm1VaSMsXJfht", + "HDQ3Vl+soSyV79Dcov2G7sPF//rf/312dvbFGDLwn9MuqLzWGmS+z1YaOHKcNZfDPXzjKcisVV0WbM2v", + "kVz4Bq9O35e5vnQ8cDfP2CuRa/WkXCnDuCe8Apa8Li0LE7Nalo7Vu9H88WVO8tDqWhRQzB3OtmuRr1nO", + "/YZgO7YVZemotjZQjG1IenVHuEPTycF1o/3ABX2+m9Gu68hOwA75x3D53+88lywK4X7iJUPRjZk6X6PE", + "iVCtVVkQ0UcXACtVzktWcMuZscox1qXSXuIhrjv3/VuBl+WIwIIt9v2WsuiMfrzPVPk0rD4poAbZgpfl", + "zN9YTtDyU2bND7yqTIYrzozlFuI2VeVaSCUhIYAcF2o9fFleKgOZVUcEsCBT4YZFIlO8YyeJY+zdGhhO", + "7j6QKIqULR2XLss9sx4BjiBYEL7mTCzZXtVsi0enFFfY36/G0fSGOeTb7gPEKua42RhxDzYjQdoLpUrg", + "0pN2RSxywvPJt/3c3k9hCffxgKLVZkqW++GW/YAfmfvIliVfnbF/rsHzPicqOWQS9uZMg621dIcSd7FQ", + "YJhU1olZlvsNjp9DI+iO4TmCaf/IytxJHRf3ysDBqLmT7JCUikYSnLMCSkBybtkN/mqsVnskFXfo50xV", + "7nir2g7ZoCz8sPS5zxWRRYy+5+KVHFl0KTYioRt4xXdiU2+YrDcLh7FlIxpa5VGDx1oDy/F0Ljo8vuIr", + "MAyc5CjoMYrzOCQ7HGrg+Xr8/iGYjlw5G77LtKplMeHNZZnSsUxrKsjFUkDBmlHGYGmnOQaPkKfB074E", + "I3DCIKPgNLMcAUfCLoFWx4jdF0RQhNUz9pOXEvCrVVcgG2GCrkVglYZroWrTdBoTLt3Uh4VJqSxklYal", + "2A2BfOu3w/FAauNFmY1/fngW0DJaNxzdK6MwRROe+sZacAPf/mXsgdF+rbSqlPFKt6N3RWj9uV0W7Sru", + "47rQcAX7pEjSPzREAo0ibO2+UN/DmG9mOMIIJ55dkkDjM3vwvE46q9goI1abeEG4r54Rp5WOnf4TXnXx", + "3KTyym6lfqQxAqmNbUVvpo+n6TBildGIA84iVu+cpLoUJUqxvzqGEjBbG3eXd3Eb5FojVpLbWsPFpfzS", + "/cUy9tZyWXBduF829NOrurTirVi5n0r66aVaifytWI1tSoA1qY7Ebhv6x42XVj/aXbPc1BThc2qGiruG", + "V7DX4Obg+RL/2S2RkPhS/04vExQjbLUcAyClgnup1FVdxRuad1TSiz178WyMWHDIQ3cI8g5TKWkAqdZz", + "2Df+N/eTuya84SOSn85/NQrf6e3YjuWBtoJG8mKc++9/aFjOLmb/47w1r5xTN3PuJ5w1egA7dv3TAebW", + "szBiXZ6pkQC1qWpL4lCKOzTH+X0DW3/OFi1q8SvkljaoC8YD2FR2/9ABHK6ju9st07kkJu5b/3L4iPtI", + "AlGGgs1w5J+M1y1UfCUkLnzOtu5psuFXjitwqewaNHO4AGODaETsj6Slxnbh5St/RZ/NUicmgVNza6S2", + "WHvpnghv8YlwFyjuaSZOwHUKpD8x32B+sLF3SQKrO8L9QaPO5eV7XlWi2F1e/tJ5pQpZwC6Nj4+K7FKt", + "soJbfjMaXT1zXRME+jnTUNdgdlcEdLfEcwIW7vdGvavtuuPDdiMe+ydnTZyK2zNVY8B+x0su8zu5Thd+", + "qMkYfiWkQCB+IPXgn2gOaG628i5Q7Hf3Tg4yGXUmH+E/kZs6w42p7NaovSuUTkLkPb8Iccq72KRPRfh/", + "UvzdUvx3pcqvyNJ2J9eVG246SnH2P1Ha3FC0e3eB0hvhcgKqDs+sdnc/r9qlZv1O7ZiQpND1wux3agef", + "6yt24WCbfizU7pmfUul/7wcmLXwKBX/nvekMGqJkvLNuyd9rrfQdYDc893vwzGcbMIavIG2CjNcYGk5Z", + "VAAYEQJuCWh0+AF4addP1/ARDmo09pHj+q7Vr9/Bxn5Ulh2ZAo6tP1rVkfd7d9gTuWw0jfncd+/zYRed", + "LZ/OEDs47bPD6Tg2pyH5QzApxTajUXN9fB05THHvDE0W30t5KZ/BUkh0erm4lI4PnS+4Ebk5rw1orzM4", + "Wyl2wfyQz7jll3I2719QY+ZXdNz00FT1ohQ5u4J9CgvkUZoYQVleRn48kXOp9zxojUpDOqNRM0cOqraZ", + "92XPNGy5LhLwmsZ3A0cmL9dDs86ZH5tcTLyvvB8/TfsDT8lhpM5BJ1Ihu16eDpE/KuudCviWESGx2oBh", + "/9rw6r2Q9heWXdaPHn0N7ElVtcaMf7UuqQ5QNGfeqWUEF4s4zGBnNc/QtSpNKKbe4E1blgzbdt1dtVpp", + "vvGuWX1H2gM7TZNPu6miZeGK3lKvD/PoZdhDFf7O1lAO3W9PRUykRrkxXo6oYg4EhLyL4pb4igtpAm83", + "YiUdVXsf8AWw3N3lUJyxF0uGvGneCXvyAVye7zUMQBhy2yYvIPR6YTmX6M6NDkJI21zu+3Z2A9YG54Y3", + "cAX7d5HTzInOF94rkR+52IraDddcbi1W2ZYbtlHoeJGDtOXeOzomSDANTC2kJY+rjoP0AJDIXdmdikgl", + "PObwHfl08qpiq1ItPO9oaPGiIcbQZ5xNvHYAmDtgEcn3dNeB/Njq6ZiNObqfvjo33q0O2cE13Zi4lkIb", + "dJcF7lk9jw/DDWjM+/IOQfnnGlCKUhp9Wrt0ZMLhTZF343aGPscgrbiGDEqxEotUdGTOOzdm8I/3HoPN", + "CIaJJRPWMK8Vd0AIyTSXK3DSCzn28ZJiuZLQlNzYbA1c2wXwEfdRREwbXtJZtuvPto5lKVkKCXO3ObBz", + "dCzcTmiQsIXCrUZo34a5O7weueoRIO+RWNwQntC99bJMz7URMvNbl/CHDvJLs7tBQA3esfFRQrjo+wYw", + "7EltHV4cFMpH7AziUWr3BE2D1nEGnehg87rTxw1yTHZLSmtq2RfKBvJTEmRqnLk1D2eqjfeB5dqGyy6M", + "Tu8ehPqMofeh36RFieEfTbwl4ZtriH1lKf5wDBwzJh6Hybtrjw/dmptw8DC6KtwTkyTWEWbWkq/joxH9", + "xu8O4eYt4ZqP7fS4uyOGSPQ9GFGEGEZHBd9qih8Pbo7BtzE4NLp/Hb+ry9Jxm1peSbV1z5lTXBbnMzry", + "Q4CvFYop9DkQhgfxCxOhxsHxj+US+UfGhCzcIcJHB7ch2E3lgmKKWp7sePnK/XjmBnDU5QaYPEKKbP2Q", + "KGErVdLA7EcVnz+5OgVICQLvFR7Gxgsm+hvSr3AU01Fip7ASIdMUl4dT7t4JHakIAcMIxQWApOgUJuSc", + "OVZ2zUvHyqwi0bQZJP3UetB5JXnB3Twce4KlNUS0IpRcTloTyTo3WU0s/geg02+TAxAv1C7DiN8hrBi4", + "W1VZw8SULPcUH9d/p+MIbj0qRwoJjudXsKfQPAwWxVOCGlnPPxZQKifpqwGFtYg6AvxtAb9DaA4L+Clq", + "Nkh6JHm3ZHcgwPPo1CPy9RjZPUAaugUAff174y/vNTxHlTJdUWZ48be34byNTyCOnGYjY0dxSPBdKkpi", + "cWR/h2q8xk35dV/6SSrrOq0YNVl4PVT0Fkrdfo4d5UoakKbGwByrclWeDbR0BkrAZ0TWEciyK0jEOr4N", + "jSO9HXsglu59/jB6HWhYCWOhE93chJS0UUZ7jAiuuLWg3fD/58F/Xbx/kv03z35/lP31f57/8sdfPjz8", + "cvDj4w9/+9v/6/709Ye/Pfyv/5iNXMvgxG21TK/pjVLNxYeNGTbuLO3eob5WFjJ892XXvEyZ957jozAp", + "aXXDrCgEX4zo3HGiK9hnhSjrNC3+2HBBUy+QUwvJgDtOyG2+Rmm6M6Nrc2A2fP+MrOolv7NFTSBn7VDf", + "HfjfhK57/PTQIU4QUwrtQ+SM7uMBtoaS0TMoyXg5niuHDlrhGp4dMhwMDkYRxj70WoygGL95aKTkWrou", + "vuOrQEs6yi3CRhGMZrCiqTqgbRM5HougW94ouT66rideXazv8aOkVSz+4y2WNxx+6vKSSc2meTsgwk5R", + "WZIANKApPCt+sCP0FNlFhpere0YY/+CgAxIJl5TFQvaFzB6dNdH103ARZAUf7K/q5iY8LMveHc1B4rFF", + "a0+RH1tqtcHDNpQ1YwXkiF6iQ3Xt1dKb1SdbG9KL45f4QDlqBwZe/h32P7u2iFXXO0iYU09Jq6YJr7zw", + "4rgVam5n80pRvh/xKOVTHMoY2WNaLrJNdCzUJ56AUq1MKmxz1UY5x1SwAPcohh3ktW3Vnj3leqP/v18Z", + "sG9ISEekRj4HlBrusKSA++PHOoKx1w17/JgI41Wl1TUvM2/LTXJzbBGsvfcsa6UP1Lvvn7x87SFGAyJw", + "nTVvjfRCsFH7xvhs1+JEDXXEGIyKqKAA6F/p3pgrTMcAvMXMKr2nqxOePBXRxrRG/OiYeoPwMojaJ5p3", + "vZMBLfGQs0Gr8CFfg65/Ab/mogwq+wBj+qqgJbWuHCffFvEAt/ZTiPxKsjvl/4PDmz4JRxhNPMOBxCkb", + "St9jmPIJUlpkuccoGgWQLDd876iF1LJDjiPrDWp2MlOKlFmsq65k2GrkPeuGclfroUHcdzNBJ9YDKxo8", + "uX0hdGNstxbKO7vVUvxWAxMFSOs+aTxzvWPoTl1Iu3fj10vCgk3p+e7x/YITnvJy8emrbrW4ZpSbvF/c", + "+yRhTSSs+fU0uLvNO6ZV4Q7lOATi8CMmdiIagPusUU0GKmosDFx2zMgneBfGMw7EhhHPwOjcSeHtHDfA", + "yvEkvOGh5NObpfnDSe+gOFvarV4/Jltq9XvKi3Y7nDaakHqlB538eumdk5FXjOglybwBipo8c7cFqXn1", + "3hqo/u3Y2DbazMwtckYP2ZjcHdtgui6pI4wczxuGgXB9efkLPSyDnZdLOmBPMcNz58mTPqaxg/I5jd8e", + "Uw/zUB/BtwueXyUW03oFdizRVrHQqckc2MXOGYscDJu2PglfBXojbJfdty+qm0q2NO1kmbYVYZGaYuHV", + "Z2wtjUoMU8stlzakUvQMzPeOSxhslTYWc+AmV1lALja8HDHvtQyyECtBuQ9rA1HmPt+fVUpIS0RTCFOV", + "fE/ulu2OvFiyR/OIeXkkFOJaGLEoAVt8RS0W3KAs0mqYQhe3KpB2bbD54wnN17UsNBR27ZNKGsWaRwcq", + "aBrPjwXYLYBkj7DdV39lD9DLxYhreOg2z8uUs4uv/ooWRvrjUZqXY7biUd4aWHqaatGnh7q6S9EPlua1", + "lJ3/pDNDXaacGGzpGf7xE7Phkq9SCdwOwEJ9Wrt+bx9kQYl2UWRiwqbnBcsd18nW3KxTSc1ztdkIu/H+", + "DkZtHLW0Oc5orjAK2fSJXTfghI/ogVyxtHLtfjU+6QzuP/INdDdxzrhhpnagtkorz9zOmM/uV1BK2lab", + "iFtCieDJI410vssoTXttl9l/snzNNc8dKzsbgzJbfPuXIaTfYdpIhlnloaC5pgN+79utwYC+nnbQgpjk", + "+7AHUsls49hD8dBz6u6ZG3VnSrPlvsPJ4SGnykhulOwwVfGIy96KvuSBAW9Jcc0yTiK7k1d27wRY6wQ1", + "/PTmpZcHNkpDV7e6CDFFHclCg9UCrjH0Io0bN+YtUaDLSZt/G+g/rQ09CIeRABVObEpUp0Dz4XZ4//Vm", + "2WOPXqWurgAqIVfn5L+NwjSN2hejF0rWIxrLSjnZSfCSYSNW8b3b5UYEPeAbvgQwWa7KEvLkG7UXfeWa", + "s4oLOjZx0tTg+HhgrhVIMMKMXOeXl+9Xa/dCcZ/dTRxpWSgggHzuzP0f0QD4SIT9CqSD+8WzY1APBu66", + "VfhcyMd0OB1/sJ98H8zSTFmsM5x3fJddOwfv65D12ido5mZ9/1sbkjKPEHZIJR34d5+6pir/w0AZHY2x", + "cFRb8zLEdiJ1L0H7skcdcFAHg4VpAJgR8uqob/7RdBVvfNtxp/rLy/daFg5zT334HPlIde3YhMwtR7sE", + "yKKFPl9zMeKTagDSE7oPbsa3SltBTjsAn9iBz2qeXyUVkO/cF9M48ZGnfeTOZyYHcqE14rXr8y7MljLG", + "ig0YyzdVcu+scTtHdwHeK277mi6OYRrIlSyMo6AcGFTKrI9lFDDpqXYSJwu50TucOVeaEgqj7GpVL9p7", + "6pYcjGvvwphppewYoA7OTkICpSzjtV27KyzEEQCWzOivhKLf8N0apZY/Y6+clBFSMfOy3M+ZsF/QONp7", + "dnK2AX1VArMagG3XygArgV9DW4oIR/vCsHc7URgsNFTCTuRqpXm1FjlTugBNNapcc3xLUyc/36Mz5qN6", + "fRzEu53E5TU1PuJ10jJD9Epj0YpXPCcRrv8zVogxUF5j4vytIiBMm9vAOOm302NRW4oZLMRyCcg9cDn4", + "FMd+7YcIJiyqhKEGzbB+TffPAwYUlpk1f/zNt2OE9vibb1O09vaHJ4+/+dZJwlwyXu9EKbjex81cqzlb", + "1KK0Pnc6Z9eQW6VjjYOQxgIvBrRF2ig/C8oyy1rm3g2t6RKXvnr7w5Nvvnr8fx9/861XX0WzhChoH2AH", + "8lpoJd2noDBsKMRP2cwGO2HsJ5CW7E5m+F5O3eoONTmiZSefUiPmAy+65tweC9uQfioc/BKKFeh5exE7", + "vtrmHHGPO6UjCXgJFCLm7kUhrVZFnQNlunjb4RsRWGIAUlNFJHK3wbMeao+1cAZNaiOzMPYCX8CP6EEm", + "VXeFeMbgGjTF9LQDPaDLIYLLWK7RTwndlvxSoXiYvtrraqV5AdO8EPCy+ol6NIkbwgjX6rQBfnbt+w+s", + "zhugI1mnBdgokAOw0FN756bunANcYvT99mYsgvI51fPSUFKoGxYIwrbzwetsCZA5QTBJ8e7VhAm4fFGT", + "Tv1aAHfX0EnHs4x1NYPQ1gRBUxBeWoOFMGU5L/O6pKfEARFym/MSLUEtYZewtMrRXlyfrzUFCDfXAj3E", + "qbIOzafdHRb1wLRR16D3vgVpXkIhG3dudM91ZygqZyVcQ5kEHLhG2eEHtWUbLvcNLtwULRjzKDKugZyE", + "YPQQIWz/5JVCEfh0zjxBHgbSoWJkc4sYzxVooQqRMyF/BX/Q46cDUgxVxFLSClljyTgNLdx01TMM0e2H", + "4Q4pQCddih1c3IIDrI3ikLDtYLuIHgrdYAhj+RUQ2CGY2Es3U3GqwYiiTkO21DzvQnYaMfrD+4ZbONcN", + "as0d0WWPeTWH/NCh69Nyj2x62Bru0iif6vDlKcyKNxFfzPPwhLO4z0AVWo5oDJRVeGlHuVuasa9Bm64b", + "cmQmgN2RsV2LzviUlyukNjh9liz4o5nR+fbEjluaC/IzBd5jf59WIbWDI0nLGgDMVth8naUCRzwA1MLB", + "8Kb/hB9OSdIFnkJYLiG3U2DAqB0qDDcKBX12UDwDXmDEeBt1RfFWfVAe/KiYG9pEIo80Ah8SrcSDozw8", + "IUd9QyHHiP9nNZH2fcA9ekJMOAZBxvG4T26Zb+OJ50UT9c7ZHgzuSuNdHp0RzEySNvGGSQso+f7QlNig", + "O2kj8wbjNt05mMDDXSjkzT4ahBym9ufs0OSuSX/BzfEcnoq4etIAkyrh5BZyaTYhVD4rYcJnM2nDcsTM", + "N0jGocj1nC06Bon7NyreTVqMdFxjCD4ZbAN+CfuAf/Q34hNbV0Jxbn9P0kp+SRNKlFQ2STJF8z0KiaY4", + "A1x/SL7Hfe3pidTUs2QFivoM9i21T99f83Ik0PINVBoM6gk4e/f9k5feKWYs3DJPRzpeXr7n1tEU9mOj", + "yaY+zGcjmSEuL98vkGNS3ocGG0PrYtKH2jEi4bq7z4PeN3PJG0uiGm1o8MUfAvT3EADGKi68o1cbazrc", + "WR90PIzunhJE1iK4vwgf1Tt6hH7gZv2c51bp/TCDq3taj6TWubx87/B9yhZ/9W2a3TsQ0pO8i/L3dFVk", + "jf8f+t4FeUgtB3l8GCbyWXOvOQt/upd+lLSn+T6bzwZ6gBYXcR7ihJ/RGj9TbkMWivMNMT2arrlYZE3Y", + "SKpI53zm0y2P1yNNaNyFyTZipVHkSY86niY6uqISNwyJ2oli6l6sGZfFe0TaWXgP4ha86EbwM6cI+oUs", + "YAe6tcy8aleXSOyfUcFZk7XK1DRvImK/X/mA4vDdFMZCcUBbszzxKJLHT+nEtEnjlzcbX2YoJstsC2K1", + "Tm/s6xsN7cTo40i7vn+kpRjcK9T6P3EHEilyhNEuWzZ8MBl6xLHRN8CO2O/tmpb/uYSkanBvmGoEXFuc", + "SAj/ObLZ/WJRCUZtxKYqyfnTs5JB7quTEk20ASYfP17proM+Pnr4BtzYI/HuozZuCsvxlFSHYzX+IZ+q", + "TVXCuPBccUni81JI/27frrllvCjQoYKXLNiAVJ7XujXi9qMxfualoMLNBrMYSqUqTFtYWSHdfzCBg6ot", + "/R+4dv8hp6Lu/4iqIjnJDTVDvGDyqzBQiOSczWfUeRYoOylFJR2TBpvSTWcV8IlO2GhLkwAFBiS02aTP", + "eW7J/umdNSXYrdJXiWfMwqA+qeNXFVfyHXJTrm1dcXqi8MaDwqdwbbLCNaB5yExtyLum4z9xlFfCrnK0", + "djqAhd5cT4Sw2Twlr0F724fyOSXJykFpagcJm5gH75Q1pVj1DRMATXJDGb7QEtvcComkGkz7NqFaS8fv", + "5MhXaOiymet9ZdU5tsEm58bqOreGvDbbOQdU6TaanJeOl0TsixROElBGkD3TqkzDNfAxNT26asFvNTgk", + "o6nONWbNACnETmXa/T2msdNbi4DErjAUNEUOduU+JMvkbs83vHpPs/zCMvaGIG5KKaBH3sasqtM9t2io", + "FOiGlzYbfeV4+ZK95aWNxQgHkPfzaDxkxhPXkgSbHD3/FE8OB9PNSdAtGIpD4v72BuL+KO/AeZuLgiSw", + "7pG6Bk2x0JPJ4efQ48N8dq/reNOc2CFXiNY3bRXxpkSsIa1iCV/DcWpTJXNZsGh+w/BsJHwF8eiCtHp/", + "kzRDYpWZUp2wvLdi9dZ1OLKlodlgT0u1BZ25eQ+guAymRoq3oZadVNJNLRcajzwloGBuMeZmG0EDn7QT", + "vsvxvWjH7jml8DJXMuvMfr9ch/hlhtSVNVkOjuwe33R3rwpv61O5FjKJvZCrdOZHx+ivYP956BISHscD", + "fKKJd1yZgw+NHxuHhsjItPVGZDISdgWdI1Uk3HMNJU1fLufAubLdc9X6F21ErhVHZ4w25TQMJFj/2ENf", + "xmY3DjmYpJXLlJibOr/bV9A45Q5L7Wx4Fd5b+A53QvDZx1RasTeNO/LQozRX0nKBBXWSwj0540JZIaNq", + "deNnnxX5/hzdzD1fk8P7k2+QgCLDVey/7f4/3DKrAe7fw/UK9lkplmDFiEG6XLqV/B32LDQ7uzOZYixH", + "Usfgh5qHkmIC2rxPTGn6ssIvcXopRnwUg6RN+MuwAizojSPFtdqyTZ2vUXbnKwgJltBgg57lvYk6o4eM", + "FN30YD4+0FQ8p4Eo7L/kegWa+Uj8pi5JMABtuMBz0noD9+Nz0VGMp4xxx9I+vaJUABHvQtNplAMqkV0q", + "gHEF+3OyDOLvN2Ak46mkRgDDvFIfEaRbpaeKc5ododerjlGVin11kr814N+hcdXB51UIJxpXh9napi4P", + "14HHoTYwXOf0aJx4bxNP3HZtUz0Dhps7YtA/ZscfKdnizb3Ix7EvQ/jYv776F9OwBI16qy+/xOG//HLu", + "/RX+9bj72VHbl1+mnZqSJ+fu/AaaSgBuDD9dkjq6BWB7NlS65A2F05LjmrvQlESXzbLshTzJgmGyARRP", + "OEaAQKkqSLbGYmXxDYoJ4DSs6pJTqI+QEnSn05RMP/T8tzvpVV3457udTLWNxUlsHW1HqkBoVIX5ZpVz", + "e+XkKM9SjhmNbjpimxOpHZGyq9xmxOeU0qUZMUSY3mbMd36MIyUcLy/fm5VEtVxQxomQJQAFYMJwl5qa", + "zAGhzGPIVNSEs8FvNS99uJ7E4Lh3mLYnvwJJFRwdl/PVdxlIU2uvEnSw4ngOFD+Mii9z0za5aS3H8YJg", + "l5fvdU7aX+/R7pNBYOYp6urEjMIhRx0uquLauyfmWDI6J9lyN5dvGOKL0Vf02NMLyVhvxm34vWzRcWQJ", + "ZlwM/UeGb+uXtKXQ07kI26SSvZuZ8t8/ePHsIRP9Yuhx1sfooXV82XEJlWkQUYaRASz93JOnQLEEGAvn", + "6QUWsiWMqIIPlu9wY+GrkOp4YKu+C/ZRKCdmVfiBG6zS4Zu3kfSfYyqFDpDsxbOknNHJjntySYj5bKVV", + "nY7cXmk0DfV9Qd0jAAUsesCTc9n542++ZYVYgbFn7J+YXI8u32FdtC42mWjrrXVKeTIErEnQSmKQD0aM", + "5lx7hA6Cg4UPSsRh7h/DN8lPPp+hXJLZXSrA/cVAZmGVj+DE3KIRv+m4vd9FWLuQVnNivplaLpP5dv+B", + "v7duETrwZA1DrE/gylew13BT2eXv2LkpMTrOeUrkPFhr52aMpwQ+EjlQ7hLH5+vHWXuCzthL15uBXCrt", + "XtWbGi19sMPEfN7gFkupmL7OtuWhMXOd/B20QqWBZMobtvtnrNlsjLLkOcrzxkcROxiaxLqNYvLBW5Rm", + "5gTkQ3qTDo8aq6UVJP64bfw52sXKXTwO6H+uRZmggkq57yaGY86kYgqdg+KWlNagzbpIMPuw8A4h3e8x", + "j9OJF2lTv6MEjId8GdXWaDUS+ZrLtmL78ZIMQ5qcVpN5UJQocczTFSPcAla0gNWdwPlpHfWkGgkPdR9Q", + "DNFAGRIb7dk9JwPi+w1Ie0PO95p6k28C1rPVh18AeuQFEHofqwN8BfvMqvTYQIYlksybpxbqSYnbRmuc", + "j7x7mhi7UAO/lV3pBDkRYVmjQTcyXQY9qX/SNf5kV7BvvV3iWoH0bLrBK4uuxbQW/J3YQPsuIUEuJQKJ", + "SVciPS/T71rKiUQs+4sDy2mGOUwVZoQqqO9hmphs543INjL0DvIc3eAURG5ImIvjQJjHvoJuYB86JjaK", + "uk6SC9QZnLFnTZIY9EOkWPs2cwzps/reipQRpcmSLHTQe3Ed9NXo0IjObnhqEozANyDZyLUZSkm+Cc+X", + "2GBMERSa7Zag23YpZUxoudS/tw2HeqDQrKrQs2BEo+VbGVuhcWgM061TZsX3syAMzuYztyz3jwPb/bvU", + "v7t/qqrEKqfVcuiTmT7AniYynCcR4j7rvlo7gmRzElvSOqIBPVhHzwfuLqkGb3OrnqqejBXolI68/eEp", + "L8t3O+n9AIdhbwc8L3lFoW8vvcdlw6EdG/fuu0Fr5blDbInhee5EvKJN+RDB+YVh/boplAhiWDnlgDfm", + "UQ7dFwFi2uR6NbpuVFgNxVCRM65XNaUfuof1HVnByMuGV6LwCRmHhey8yEZsodZQMKV9Ki+x9Hnaxio5", + "HC9TRbtXeZlR5K1o2GahGKH0uXv8QOWTrSuZ5Y1nubsn3QvTKnZJHtmXszP2gnLGaOAFMVgtLKTqKHXW", + "j8lvt4BlnQNFZw12oyp4Z+4UdWpuGaRsDeg/kSiR9m9ZjwsxZuoRjI1xJZKqukj6BBh6OiwmhsUCpLL/", + "RniaVJnr8vI9VHiwumUv4jiKqmqKdZXg9v23GgPgHMPGYUd0tEqDWMmRWuxIIEseLgLTR1fyOuhyKZ9u", + "MEa8GdwSjTh+MyaKlhcajFII8CLDUvYHXL4T7LXZi5Hi8MTgmmSTpo29MX6VUQ2MaUsMbOZ1tEIk7CDK", + "3uX6blBS7dZ11HoDdLjGsb6dAKNE5bX4LuwPfUwyi6ycByUzKtlQuoUTf9KQhfszcCxZUDWHuo1XupRP", + "2O+glX+sNkO5A9Hqxn0acJ8f9SzRqSmsYgbd+lOeWLCGFn9AOhwtAHV5+X7HB1IGwnQL+eJmNbyO4vj5", + "SCmRGMfBVOZriNyyEhDNeGBj25jLoUWMF7ivUf2F2MeLmExTUIB229dUQWLh25EyJgexuTyIzQPjdzIw", + "bcPrkNIPp9mnf01Srqtt2HHqkYrrHI9RbCtKDaeecvgb54FJpBFeyLcljjDrAfIYN6VzTl6iT8iI7l5l", + "xgteAb4z5llIOpG3gXIZuFmwzQXrcUxp7maie23DqzutHneUeUQQj/scwKjHQZvXzF/MiVTmNELr2+Bk", + "zWCNTIiMJ649jJ5GIX7tp7PicVUIs1Z1WVBhiA3mYmvfmAns+AJQjVzYFuQiNw70uoiDrE00Q7zZjL1w", + "I/Nyy/cmKGpbyhofLuwqlY9IKAnjZI2kXU7vjc7JTRxyUQmQtvG5ifHiiHxcvZke2KtJHdehLHLiutFa", + "eMd73lZS65reguXNV4vi0Q0999vMy666gAYOqmjX5mkYO6yoQWl0oR1PKZKqp9ds6RGm522jB7md1yue", + "yuSoF3E5mmacvUkluwHAI0YZ6Ro5pL3i+qpzCfrD6geQK0on0Bm1I2NESQAMlJSKtBeDPBYhY6D0pozX", + "9aIUOZoR0Om7MSx4j/+CveGyUBv2PCTzefDzm+cPmQZTlzYQWchs7IjPQ/JpywmMLrzSS7/yt1G0TLN8", + "Ib1FZSWM1QnF5b2vCnM+HnM4co2WxrZeR2SwpnSPg4Bw4blg+hrCCa9gnxWirEcJ2bW6KroJN029wLJw", + "QlJW3gW3OXqzDEAwB6Y+4uHg2pS0VHRzuO1Kpx0YXK4/MZ1Zqt75+dwI6MhTIphXD3NPb7k5lX36bsQ/", + "/Uw3kw9JPGzDJKJEwA6foSBK7+K/lZQVTUFxWk76ML4qYStsdV1K26KfsvEMjQwJR11Ou+Ol3U6DnIWT", + "YOEzMZS43IR4+/u7pZWMsH/hi5WWkfCzrGVhelvYlsM/YH89KPt40Se0OWjKHRMKpkoCnaDZLiRouPRB", + "J228tDEqF60RHmtNUlXJf8hy75PS9St6tFtZaXUtilQh+lKtRG5IBXOqxfhl6PthPtvUpRU3HOdV6Esm", + "7PR1KFb+KpQF1wWD4vE333z1124qhM+IXQ03Kene45fltYzcirwrxzarm8DEAirPVmrIskaNbXrV2h4a", + "41oqcet0GxkCMh76HhSt3kFksWc8InXlxPbSivanufttzc26ZZ1R2WIsI82Z51d9rz+ML4oMffccfu4J", + "O7uVY0bveIwxjvaQfA5nI2aPRA9TWeKriJMMVrjxSyS9q6OXEHSJe12V4GS7lgeOptEJqKErP8z5Vgyr", + "/MfjpXcdG2DpQOUkEcrL6oTJVuJCBUEL1Q28gwf78zaGK5UXb63BOIjS3jdrncw0cij/Zpv5MJFX/STc", + "vu3taS8zCe7bqIRbXX2iBDaHaODzyOKQdsQ6LDKP5WJgUwLzmmRU/SRU49JzlBX2EOmP5lvtvp+nZzTx", + "4PS93Mbc00wVHNTeRaGjcYYu9oLIv/VqRDlWUr4an3KPjL++AEB3v24fkv8BIwSWirIbSMtz26YWnz3x", + "I818kd/Z2trKXJyfb7fbszDNWa425yuMcsqsqvP1eRgI00h2Uqf5Lr76lbt2y70VuWFPXr9AIVnYEjBg", + "AlEXJdS9mD0+e0SpF0HySswuZl+fPTr7io7IGuninNIcu/+uKMzBUQ1Kwi8KDEG/gjhRMtbOxlTI2P3x", + "o0dhG/wzMTJPnv9qiKFNs5jG0+AmdzfiAdrTHkYF+RMVlOWVVFvJvtdaEYM09WbD9R4joG2tpWGPHz1i", + "YunTO1PiD+7EtPczisid/eL6nV8/Po/8xHq/nP8RXDRE8eHI53NeVSaLDMhH2wcr/MFWiSi+6X0mzdAr", + "QBnapueLfj3/o2ui/jCx2fkCK0VMbQpTpz/3fv6hbX/x+Pf5H0G1/OHAp3OfluJQ95F96+Sn7v1szv8g", + "r2rSYEQQpMfq3AZ/2J0HGhW9+hrHf/9Hj/3Ajm+qEpDzzD780lB9w7g89X+YN7+USl3VVfyLAa7zNXbf", + "ZUqLlZCOqrd8tQKd9fjO/w8AAP//62VeisflAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/generated/common/types.go b/api/generated/common/types.go index 3a0e083ca..a7c59f989 100644 --- a/api/generated/common/types.go +++ b/api/generated/common/types.go @@ -1144,6 +1144,9 @@ type TransactionStateProof struct { StateProofType *uint64 `json:"state-proof-type,omitempty"` } +// Absent defines model for absent. +type Absent = []string + // AccountId defines model for account-id. type AccountId = string @@ -1183,6 +1186,9 @@ type Exclude = []string // ExcludeCloseTo defines model for exclude-close-to. type ExcludeCloseTo = bool +// Expired defines model for expired. +type Expired = []string + // HeaderOnly defines model for header-only. type HeaderOnly = bool @@ -1204,6 +1210,9 @@ type Next = string // NotePrefix defines model for note-prefix. type NotePrefix = string +// Proposers defines model for proposers. +type Proposers = []string + // RekeyTo defines model for rekey-to. type RekeyTo = bool @@ -1334,6 +1343,17 @@ type AssetsResponse struct { NextToken *string `json:"next-token,omitempty"` } +// BlockHeadersResponse defines model for BlockHeadersResponse. +type BlockHeadersResponse struct { + Blocks []Block `json:"blocks"` + + // CurrentRound Round at which the results were computed. + CurrentRound uint64 `json:"current-round"` + + // NextToken Used for pagination, when making another request provide this token with the next parameter. + NextToken *string `json:"next-token,omitempty"` +} + // BlockResponse Block information. // // Definition: diff --git a/api/generated/v2/routes.go b/api/generated/v2/routes.go index 6b9f38d0d..ae2f05591 100644 --- a/api/generated/v2/routes.go +++ b/api/generated/v2/routes.go @@ -69,6 +69,9 @@ type ServerInterface interface { // (GET /v2/assets/{asset-id}/transactions) LookupAssetTransactions(ctx echo.Context, assetId uint64, params LookupAssetTransactionsParams) error + // (GET /v2/block-headers) + SearchForBlockHeaders(ctx echo.Context, params SearchForBlockHeadersParams) error + // (GET /v2/blocks/{round-number}) LookupBlock(ctx echo.Context, roundNumber uint64, params LookupBlockParams) error @@ -974,6 +977,80 @@ func (w *ServerInterfaceWrapper) LookupAssetTransactions(ctx echo.Context) error return err } +// SearchForBlockHeaders converts echo context to params. +func (w *ServerInterfaceWrapper) SearchForBlockHeaders(ctx echo.Context) error { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params SearchForBlockHeadersParams + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", ctx.QueryParams(), ¶ms.Limit) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) + } + + // ------------- Optional query parameter "next" ------------- + + err = runtime.BindQueryParameter("form", true, false, "next", ctx.QueryParams(), ¶ms.Next) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter next: %s", err)) + } + + // ------------- Optional query parameter "min-round" ------------- + + err = runtime.BindQueryParameter("form", true, false, "min-round", ctx.QueryParams(), ¶ms.MinRound) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter min-round: %s", err)) + } + + // ------------- Optional query parameter "max-round" ------------- + + err = runtime.BindQueryParameter("form", true, false, "max-round", ctx.QueryParams(), ¶ms.MaxRound) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter max-round: %s", err)) + } + + // ------------- Optional query parameter "before-time" ------------- + + err = runtime.BindQueryParameter("form", true, false, "before-time", ctx.QueryParams(), ¶ms.BeforeTime) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter before-time: %s", err)) + } + + // ------------- Optional query parameter "after-time" ------------- + + err = runtime.BindQueryParameter("form", true, false, "after-time", ctx.QueryParams(), ¶ms.AfterTime) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter after-time: %s", err)) + } + + // ------------- Optional query parameter "proposers" ------------- + + err = runtime.BindQueryParameter("form", false, false, "proposers", ctx.QueryParams(), ¶ms.Proposers) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter proposers: %s", err)) + } + + // ------------- Optional query parameter "expired" ------------- + + err = runtime.BindQueryParameter("form", false, false, "expired", ctx.QueryParams(), ¶ms.Expired) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter expired: %s", err)) + } + + // ------------- Optional query parameter "absent" ------------- + + err = runtime.BindQueryParameter("form", false, false, "absent", ctx.QueryParams(), ¶ms.Absent) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter absent: %s", err)) + } + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.SearchForBlockHeaders(ctx, params) + return err +} + // LookupBlock converts echo context to params. func (w *ServerInterfaceWrapper) LookupBlock(ctx echo.Context) error { var err error @@ -1203,6 +1280,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/v2/assets/:asset-id", wrapper.LookupAssetByID, m...) router.GET(baseURL+"/v2/assets/:asset-id/balances", wrapper.LookupAssetBalances, m...) router.GET(baseURL+"/v2/assets/:asset-id/transactions", wrapper.LookupAssetTransactions, m...) + router.GET(baseURL+"/v2/block-headers", wrapper.SearchForBlockHeaders, m...) router.GET(baseURL+"/v2/blocks/:round-number", wrapper.LookupBlock, m...) router.GET(baseURL+"/v2/transactions", wrapper.SearchForTransactions, m...) router.GET(baseURL+"/v2/transactions/:txid", wrapper.LookupTransaction, m...) @@ -1212,229 +1290,234 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "lrsEplHBUCfD7ulxPOC1avkiYIHke/RC2D+7HX2PFvGo89WCm6H57LfJ/UcLnsK/TuFfJ9H0Pr0H4JDP", - "//DXc7/HAFzzMUnObcPx0mRcsfzkK3CnvgJA5sbSwntMKg1TnsjNSZn3abs6dCnm+YyWVORsr0YOWW9t", - "QA3t6/aslxIIisuHDwRmJ0X1k51ko5NsdCpddwpsGhvYdDSm67jcSEw8R0lpr7ngp2ydqVdv1jwNJ5Ht", - "z8SAHJLqomWeAF2so0+78l1glgv7pGLmi50y3ynbxSnbxSnbxSnbxSnbxcexRp/yUpzyUpzEt3/vvBRj", - "PE6cEdMCKgVDV+ZWY3z+B7mQu3ZC6S3qmVzNuGCNAORX0BQLNdIeFDRaUhPeYd/QSKKDl8GedWVKlgPv", - "KzjhgFCcM34N/50rxn5nmaHKMtdj3tvWajyAUBozmj+ujXnQ2ixTjAo34vOBaFdGVa0gDa0JuWoJJX4l", - "U8snb2VN1nBZSn4F/V1dTbvpKyjI2qnRaiQxqh40TrvuGcCzN/PI9D4MQKckKqckKqckKn8CbcislPmV", - "Pv8DjjpDPcJeIzZ0GlJiPLUf9yku8DLidOm0UDFAtyRqPzBaMEWkffTnJV2ckX/Yywm3D1xLjafQ00Zn", - "A2skhWSoC3EKgC4PoAfo3xKmzOyUd0sCd0arwEmcAsM/4+s5SjUZeYaOzcDb1Uh6dj3NNnIN7HiXaQ9i", - "4mG5fYOX6knTedJ0njSdJ03nSdN5yut70p+e9Kcn/elJf3rSn570p3euP/2YOs+7rxV60qqetKontc1H", - "DQuKj/b8DysT7Q8MIlZ8LFsv5JCKNca6MdFBTii7vxxq90hCou066LKOv5ynGJoTeflUtMIfphPN1LW/", - "67UqJxeTpTGVvjg/Zxu6qkp2lsvVOSSpcP3/CHy/XK3goQq/uJGjXxwps903mVTcvr1lptd0sWAqszMj", - "zI/PHk0+/J8AAAD//3BbS4jifQEA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/generated/v2/types.go b/api/generated/v2/types.go index 36d8da56d..4916b726c 100644 --- a/api/generated/v2/types.go +++ b/api/generated/v2/types.go @@ -1232,6 +1232,9 @@ type TransactionStateProof struct { StateProofType *uint64 `json:"state-proof-type,omitempty"` } +// Absent defines model for absent. +type Absent = []string + // AccountId defines model for account-id. type AccountId = string @@ -1271,6 +1274,9 @@ type Exclude = []string // ExcludeCloseTo defines model for exclude-close-to. type ExcludeCloseTo = bool +// Expired defines model for expired. +type Expired = []string + // HeaderOnly defines model for header-only. type HeaderOnly = bool @@ -1292,6 +1298,9 @@ type Next = string // NotePrefix defines model for note-prefix. type NotePrefix = string +// Proposers defines model for proposers. +type Proposers = []string + // RekeyTo defines model for rekey-to. type RekeyTo = bool @@ -1422,6 +1431,17 @@ type AssetsResponse struct { NextToken *string `json:"next-token,omitempty"` } +// BlockHeadersResponse defines model for BlockHeadersResponse. +type BlockHeadersResponse struct { + Blocks []Block `json:"blocks"` + + // CurrentRound Round at which the results were computed. + CurrentRound uint64 `json:"current-round"` + + // NextToken Used for pagination, when making another request provide this token with the next parameter. + NextToken *string `json:"next-token,omitempty"` +} + // BlockResponse Block information. // // Definition: @@ -1811,6 +1831,36 @@ type LookupAssetTransactionsParamsSigType string // LookupAssetTransactionsParamsAddressRole defines parameters for LookupAssetTransactions. type LookupAssetTransactionsParamsAddressRole string +// SearchForBlockHeadersParams defines parameters for SearchForBlockHeaders. +type SearchForBlockHeadersParams struct { + // Limit Maximum number of results to return. There could be additional pages even if the limit is not reached. + Limit *uint64 `form:"limit,omitempty" json:"limit,omitempty"` + + // Next The next page of results. Use the next token provided by the previous results. + Next *string `form:"next,omitempty" json:"next,omitempty"` + + // MinRound Include results at or after the specified min-round. + MinRound *uint64 `form:"min-round,omitempty" json:"min-round,omitempty"` + + // MaxRound Include results at or before the specified max-round. + MaxRound *uint64 `form:"max-round,omitempty" json:"max-round,omitempty"` + + // BeforeTime Include results before the given time. Must be an RFC 3339 formatted string. + BeforeTime *time.Time `form:"before-time,omitempty" json:"before-time,omitempty"` + + // AfterTime Include results after the given time. Must be an RFC 3339 formatted string. + AfterTime *time.Time `form:"after-time,omitempty" json:"after-time,omitempty"` + + // Proposers Accounts marked as proposer in the block header's participation updates. This parameter accepts a comma separated list of addresses. + Proposers *[]string `form:"proposers,omitempty" json:"proposers,omitempty"` + + // Expired Accounts marked as expired in the block header's participation updates. This parameter accepts a comma separated list of addresses. + Expired *[]string `form:"expired,omitempty" json:"expired,omitempty"` + + // Absent Accounts marked as absent in the block header's participation updates. This parameter accepts a comma separated list of addresses. + Absent *[]string `form:"absent,omitempty" json:"absent,omitempty"` +} + // LookupBlockParams defines parameters for LookupBlock. type LookupBlockParams struct { // HeaderOnly Header only flag. When this is set to true, returned block does not contain the transactions diff --git a/api/handlers.go b/api/handlers.go index 76d40f9c0..6449c3d69 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -45,6 +45,27 @@ type ServerImplementation struct { // Helper functions // ////////////////////// +func validateBlockFilter(filter *idb.BlockHeaderFilter) error { + var errorArr = make([]string, 0) + + // Int64 overflows + if (filter.MaxRound != nil && *filter.MaxRound > math.MaxInt64) || + (filter.MinRound != nil && *filter.MinRound > math.MaxInt64) { + errorArr = append(errorArr, errValueExceedingInt64) + } + + // Time + if !filter.AfterTime.IsZero() && !filter.BeforeTime.IsZero() && filter.AfterTime.After(filter.BeforeTime) { + errorArr = append(errorArr, errInvalidTimeMinMax) + } + + if len(errorArr) > 0 { + return errors.New("invalid input: " + strings.Join(errorArr, ", ")) + } + + return nil +} + func validateTransactionFilter(filter *idb.TransactionFilter) error { var errorArr = make([]string, 0) @@ -193,9 +214,9 @@ func (si *ServerImplementation) LookupAccountByID(ctx echo.Context, accountID st return badRequest(ctx, errRewindingAccountNotSupported) } - addr, decodeErrors := decodeAddress(&accountID, "account-id", make([]string, 0)) - if len(decodeErrors) != 0 { - return badRequest(ctx, decodeErrors[0]) + addr, err := sdk.DecodeAddress(accountID) + if err != nil { + return badRequest(ctx, fmt.Sprintf("%s: %v", errUnableToParseAddress, err)) } options := idb.AccountQueryOptions{ @@ -293,9 +314,9 @@ func (si *ServerImplementation) LookupAccountAssets(ctx echo.Context, accountID return notFound(ctx, errValueExceedingInt64) } - addr, errors := decodeAddress(&accountID, "account-id", make([]string, 0)) - if len(errors) != 0 { - return badRequest(ctx, errors[0]) + addr, err := sdk.DecodeAddress(accountID) + if err != nil { + return badRequest(ctx, fmt.Sprintf("%s: %v", errUnableToParseAddress, err)) } var assetGreaterThan *uint64 @@ -308,7 +329,7 @@ func (si *ServerImplementation) LookupAccountAssets(ctx echo.Context, accountID } query := idb.AssetBalanceQuery{ - Address: addr, + Address: addr[:], AssetID: params.AssetId, AssetIDGT: assetGreaterThan, IncludeDeleted: boolOrDefault(params.IncludeAll), @@ -387,9 +408,13 @@ func (si *ServerImplementation) SearchForAccounts(ctx echo.Context, params gener return badRequest(ctx, errRewindingAccountNotSupported) } - spendingAddr, decodeErrors := decodeAddress(params.AuthAddr, "account-id", make([]string, 0)) - if len(decodeErrors) != 0 { - return badRequest(ctx, decodeErrors[0]) + var spendingAddrBytes []byte + if params.AuthAddr != nil { + spendingAddr, err := sdk.DecodeAddress(*params.AuthAddr) + if err != nil { + return badRequest(ctx, fmt.Sprintf("unable to parse auth addr: %v", err)) + } + spendingAddrBytes = spendingAddr[:] } options := idb.AccountQueryOptions{ @@ -400,7 +425,7 @@ func (si *ServerImplementation) SearchForAccounts(ctx echo.Context, params gener Limit: min(uintOrDefaultValue(params.Limit, si.opts.DefaultAccountsLimit), si.opts.MaxAccountsLimit), HasAssetID: uintOrDefault(params.AssetId), HasAppID: uintOrDefault(params.ApplicationId), - EqualToAuthAddr: spendingAddr[:], + EqualToAuthAddr: spendingAddrBytes, IncludeDeleted: boolOrDefault(params.IncludeAll), MaxResources: si.opts.MaxAPIResourcesPerAccount, } @@ -465,10 +490,11 @@ func (si *ServerImplementation) LookupAccountTransactions(ctx echo.Context, acco if (params.AssetId != nil && uint64(*params.AssetId) > math.MaxInt64) || (params.Round != nil && uint64(*params.Round) > math.MaxInt64) { return notFound(ctx, errValueExceedingInt64) } + // Check that a valid account was provided - _, errors := decodeAddress(strPtr(accountID), "account-id", make([]string, 0)) - if len(errors) != 0 { - return badRequest(ctx, errors[0]) + _, err := sdk.DecodeAddress(accountID) + if err != nil { + return badRequest(ctx, fmt.Sprintf("%s: %v", errUnableToParseAddress, err)) } searchParams := generated.SearchForTransactionsParams{ @@ -981,6 +1007,79 @@ func (si *ServerImplementation) LookupTransaction(ctx echo.Context, txid string) return ctx.JSON(http.StatusOK, response) } +// SearchForBlockHeaders returns block headers matching the provided parameters +// (GET /v2/blocks) +func (si *ServerImplementation) SearchForBlockHeaders(ctx echo.Context, params generated.SearchForBlockHeadersParams) error { + // Validate query parameters + if err := si.verifyHandler("SearchForBlockHeaders", ctx); err != nil { + return badRequest(ctx, err.Error()) + } + + // Convert query params into a filter + filter, err := si.blockParamsToBlockFilter(params) + if err != nil { + return badRequest(ctx, err.Error()) + } + err = validateBlockFilter(&filter) + if err != nil { + return badRequest(ctx, err.Error()) + } + + // Fetch the block headers + blockHeaders, next, round, err := si.fetchBlockHeaders(ctx.Request().Context(), filter) + if err != nil { + return indexerError(ctx, fmt.Errorf("%s: %w", errBlockHeaderSearch, err)) + } + + // Populate the response model and render it + response := generated.BlockHeadersResponse{ + CurrentRound: round, + NextToken: strPtr(next), + Blocks: blockHeaders, + } + return ctx.JSON(http.StatusOK, response) +} + +// fetchBlockHeaders is used to query the backend for block headers, and compute the next token +func (si *ServerImplementation) fetchBlockHeaders(ctx context.Context, bf idb.BlockHeaderFilter) ([]generated.Block, string, uint64 /*round*/, error) { + + var round uint64 + var nextToken string + results := make([]generated.Block, 0) + err := callWithTimeout(ctx, si.log, si.timeout, func(ctx context.Context) error { + + // Open a channel from which result rows will be received + var rows <-chan idb.BlockRow + rows, round = si.db.BlockHeaders(ctx, bf) + + // Iterate received rows, converting each to a generated.Block + var lastRow idb.BlockRow + for row := range rows { + if row.Error != nil { + return row.Error + } + + results = append(results, hdrRowToBlock(row)) + lastRow = row + } + + // No next token if there were no results. + if len(results) == 0 { + return nil + } + + // Generate the next token and return + var err error + nextToken, err = lastRow.Next() + return err + }) + if err != nil { + return nil, "", 0, err + } + + return results, nextToken, round, nil +} + // SearchForTransactions returns transactions matching the provided parameters // (GET /v2/transactions) func (si *ServerImplementation) SearchForTransactions(ctx echo.Context, params generated.SearchForTransactionsParams) error { diff --git a/api/handlers_test.go b/api/handlers_test.go index a64aa31e0..c18323513 100644 --- a/api/handlers_test.go +++ b/api/handlers_test.go @@ -846,7 +846,7 @@ func TestTimeouts(t *testing.T) { errString: errTransactionSearch, mockCall: transactionFunc, callHandler: func(ctx echo.Context, si ServerImplementation) error { - return si.LookupAccountTransactions(ctx, "", generated.LookupAccountTransactionsParams{}) + return si.LookupAccountTransactions(ctx, "MONEYMBRSMUAM2NGL6PCEQEDVHFWAQB6DU47NUS6P5DJM4OJFN7E7DSVBA", generated.LookupAccountTransactionsParams{}) }, }, { diff --git a/api/indexer.oas2.json b/api/indexer.oas2.json index 76c11d21d..08061df94 100644 --- a/api/indexer.oas2.json +++ b/api/indexer.oas2.json @@ -853,6 +853,61 @@ } } }, + "/v2/block-headers": { + "get": { + "description": "Search for block headers. Block headers are returned in ascending round order. Transactions are not included in the output.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "operationId": "searchForBlockHeaders", + "parameters": [ + { + "$ref": "#/parameters/limit" + }, + { + "$ref": "#/parameters/next" + }, + { + "$ref": "#/parameters/min-round" + }, + { + "$ref": "#/parameters/max-round" + }, + { + "$ref": "#/parameters/before-time" + }, + { + "$ref": "#/parameters/after-time" + }, + { + "$ref": "#/parameters/proposers" + }, + { + "$ref": "#/parameters/expired" + }, + { + "$ref": "#/parameters/absent" + } + ], + "responses": { + "200": { + "$ref": "#/responses/BlockHeadersResponse" + }, + "404": { + "$ref": "#/responses/ErrorResponse" + }, + "500": { + "$ref": "#/responses/ErrorResponse" + } + } + } + }, "/v2/blocks/{round-number}": { "get": { "description": "Lookup block.", @@ -1753,7 +1808,7 @@ "Box": { "description": "Box name and its content.", "required": [ - "name", + "name", "value", "round" ], @@ -2595,10 +2650,10 @@ "StateProofParticipant": { "type": "object", "properties": { - "verifier": { - "description": "\\[p\\]", - "$ref": "#/definitions/StateProofVerifier" - }, + "verifier": { + "description": "\\[p\\]", + "$ref": "#/definitions/StateProofVerifier" + }, "weight": { "description": "\\[w\\]", "type": "integer", @@ -2677,6 +2732,39 @@ } }, "parameters": { + "proposers": { + "type": "array", + "items": { + "type": "string", + "x-algorand-format": "Address" + }, + "description": "Accounts marked as proposer in the block header's participation updates. This parameter accepts a comma separated list of addresses.", + "name": "proposers", + "in": "query", + "required": false + }, + "absent": { + "type": "array", + "items": { + "type": "string", + "x-algorand-format": "Address" + }, + "description": "Accounts marked as absent in the block header's participation updates. This parameter accepts a comma separated list of addresses.", + "name": "absent", + "in": "query", + "required": false + }, + "expired": { + "type": "array", + "items": { + "type": "string", + "x-algorand-format": "Address" + }, + "description": "Accounts marked as expired in the block header's participation updates. This parameter accepts a comma separated list of addresses.", + "name": "expired", + "in": "query", + "required": false + }, "account-id": { "type": "string", "description": "account string", @@ -3103,7 +3191,7 @@ "schema": { "type": "object", "required": [ - "application-id", + "application-id", "boxes" ], "properties": { @@ -3132,7 +3220,7 @@ }, "ErrorResponse": { "description": "Response for errors", - "schema":{ + "schema": { "type": "object", "required": [ "message" @@ -3179,6 +3267,32 @@ "$ref": "#/definitions/Block" } }, + "BlockHeadersResponse": { + "description": "(empty)", + "schema": { + "type": "object", + "required": [ + "current-round", + "blocks" + ], + "properties": { + "current-round": { + "description": "Round at which the results were computed.", + "type": "integer" + }, + "next-token": { + "description": "Used for pagination, when making another request provide this token with the next parameter.", + "type": "string" + }, + "blocks": { + "type": "array", + "items": { + "$ref": "#/definitions/Block" + } + } + } + } + }, "HealthCheckResponse": { "description": "(empty)", "schema": { @@ -3242,4 +3356,4 @@ "name": "search" } ] -} +} \ No newline at end of file diff --git a/api/indexer.oas3.yml b/api/indexer.oas3.yml index 9a425b354..0e2c96185 100644 --- a/api/indexer.oas3.yml +++ b/api/indexer.oas3.yml @@ -1,6 +1,20 @@ { "components": { "parameters": { + "absent": { + "description": "Accounts marked as absent in the block header's participation updates. This parameter accepts a comma separated list of addresses.", + "explode": false, + "in": "query", + "name": "absent", + "schema": { + "items": { + "type": "string", + "x-algorand-format": "Address" + }, + "type": "array" + }, + "style": "form" + }, "account-id": { "description": "account string", "in": "path", @@ -135,6 +149,20 @@ "type": "boolean" } }, + "expired": { + "description": "Accounts marked as expired in the block header's participation updates. This parameter accepts a comma separated list of addresses.", + "explode": false, + "in": "query", + "name": "expired", + "schema": { + "items": { + "type": "string", + "x-algorand-format": "Address" + }, + "type": "array" + }, + "style": "form" + }, "header-only": { "description": "Header only flag. When this is set to true, returned block does not contain the transactions", "in": "query", @@ -193,6 +221,20 @@ }, "x-algorand-format": "base64" }, + "proposers": { + "description": "Accounts marked as proposer in the block header's participation updates. This parameter accepts a comma separated list of addresses.", + "explode": false, + "in": "query", + "name": "proposers", + "schema": { + "items": { + "type": "string", + "x-algorand-format": "Address" + }, + "type": "array" + }, + "style": "form" + }, "rekey-to": { "description": "Include results which include the rekey-to field.", "in": "query", @@ -549,6 +591,36 @@ }, "description": "(empty)" }, + "BlockHeadersResponse": { + "content": { + "application/json": { + "schema": { + "properties": { + "blocks": { + "items": { + "$ref": "#/components/schemas/Block" + }, + "type": "array" + }, + "current-round": { + "description": "Round at which the results were computed.", + "type": "integer" + }, + "next-token": { + "description": "Used for pagination, when making another request provide this token with the next parameter.", + "type": "string" + } + }, + "required": [ + "blocks", + "current-round" + ], + "type": "object" + } + } + }, + "description": "(empty)" + }, "BlockResponse": { "content": { "application/json": { @@ -4763,6 +4835,189 @@ ] } }, + "/v2/block-headers": { + "get": { + "description": "Search for block headers. Block headers are returned in ascending round order. Transactions are not included in the output.", + "operationId": "searchForBlockHeaders", + "parameters": [ + { + "description": "Maximum number of results to return. There could be additional pages even if the limit is not reached.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer" + } + }, + { + "description": "The next page of results. Use the next token provided by the previous results.", + "in": "query", + "name": "next", + "schema": { + "type": "string" + } + }, + { + "description": "Include results at or after the specified min-round.", + "in": "query", + "name": "min-round", + "schema": { + "type": "integer" + } + }, + { + "description": "Include results at or before the specified max-round.", + "in": "query", + "name": "max-round", + "schema": { + "type": "integer" + } + }, + { + "description": "Include results before the given time. Must be an RFC 3339 formatted string.", + "in": "query", + "name": "before-time", + "schema": { + "format": "date-time", + "type": "string", + "x-algorand-format": "RFC3339 String" + }, + "x-algorand-format": "RFC3339 String" + }, + { + "description": "Include results after the given time. Must be an RFC 3339 formatted string.", + "in": "query", + "name": "after-time", + "schema": { + "format": "date-time", + "type": "string", + "x-algorand-format": "RFC3339 String" + }, + "x-algorand-format": "RFC3339 String" + }, + { + "description": "Accounts marked as proposer in the block header's participation updates. This parameter accepts a comma separated list of addresses.", + "explode": false, + "in": "query", + "name": "proposers", + "schema": { + "items": { + "type": "string", + "x-algorand-format": "Address" + }, + "type": "array" + }, + "style": "form" + }, + { + "description": "Accounts marked as expired in the block header's participation updates. This parameter accepts a comma separated list of addresses.", + "explode": false, + "in": "query", + "name": "expired", + "schema": { + "items": { + "type": "string", + "x-algorand-format": "Address" + }, + "type": "array" + }, + "style": "form" + }, + { + "description": "Accounts marked as absent in the block header's participation updates. This parameter accepts a comma separated list of addresses.", + "explode": false, + "in": "query", + "name": "absent", + "schema": { + "items": { + "type": "string", + "x-algorand-format": "Address" + }, + "type": "array" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "blocks": { + "items": { + "$ref": "#/components/schemas/Block" + }, + "type": "array" + }, + "current-round": { + "description": "Round at which the results were computed.", + "type": "integer" + }, + "next-token": { + "description": "Used for pagination, when making another request provide this token with the next parameter.", + "type": "string" + } + }, + "required": [ + "blocks", + "current-round" + ], + "type": "object" + } + } + }, + "description": "(empty)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + } + }, + "tags": [ + "search" + ] + } + }, "/v2/blocks/{round-number}": { "get": { "description": "Lookup block.", diff --git a/api/server.go b/api/server.go index c8f21531e..aadd58420 100644 --- a/api/server.go +++ b/api/server.go @@ -47,10 +47,20 @@ type ExtraOptions struct { // If an address exceeds this number, a 400 error is returned. Zero means unlimited. MaxAPIResourcesPerAccount uint64 + // MaxAccountListSize is the maximum number of items that can be passed in query parameter account lists. + // (e.g.: GET /v2/block-headers?proposer=A,B,C) + // + // Zero means unlimited. + MaxAccountListSize uint64 + ///////////////////// // Limit Constants // ///////////////////// + // Blocks + MaxBlocksLimit uint64 + DefaultBlocksLimit uint64 + // Transactions MaxTransactionsLimit uint64 DefaultTransactionsLimit uint64 diff --git a/cmd/algorand-indexer/daemon.go b/cmd/algorand-indexer/daemon.go index 421bfd4fb..213f21892 100644 --- a/cmd/algorand-indexer/daemon.go +++ b/cmd/algorand-indexer/daemon.go @@ -33,6 +33,9 @@ type daemonConfig struct { readTimeout time.Duration maxConn uint32 maxAPIResourcesPerAccount uint32 + maxAccountListSize uint32 + maxBlocksLimit uint32 + defaultBlocksLimit uint32 maxTransactionsLimit uint32 defaultTransactionsLimit uint32 maxAccountsLimit uint32 @@ -81,6 +84,9 @@ func DaemonCmd() *cobra.Command { cfg.flags.StringVar(&cfg.suppliedAPIConfigFile, "api-config-file", "", "supply an API config file to enable/disable parameters") cfg.flags.BoolVar(&cfg.enableAllParameters, "enable-all-parameters", false, "override default configuration and enable all parameters. Can't be used with --api-config-file") cfg.flags.Uint32VarP(&cfg.maxAPIResourcesPerAccount, "max-api-resources-per-account", "", 1000, "set the maximum total number of resources (created assets, created apps, asset holdings, and application local state) per account that will be allowed in REST API lookupAccountByID and searchForAccounts responses before returning a 400 Bad Request. Set zero for no limit") + cfg.flags.Uint32VarP(&cfg.maxAccountListSize, "max-account-list-size", "", 50, "set the maximum number of items for query parameters that accept account lists. Set zero for no limit") + cfg.flags.Uint32VarP(&cfg.maxBlocksLimit, "max-blocks-limit", "", 1000, "set the maximum allowed Limit parameter for querying blocks") + cfg.flags.Uint32VarP(&cfg.defaultBlocksLimit, "default-blocks-limit", "", 100, "set the default Limit parameter for querying blocks, if none is provided") cfg.flags.Uint32VarP(&cfg.maxTransactionsLimit, "max-transactions-limit", "", 10000, "set the maximum allowed Limit parameter for querying transactions") cfg.flags.Uint32VarP(&cfg.defaultTransactionsLimit, "default-transactions-limit", "", 1000, "set the default Limit parameter for querying transactions, if none is provided") cfg.flags.Uint32VarP(&cfg.maxAccountsLimit, "max-accounts-limit", "", 1000, "set the maximum allowed Limit parameter for querying accounts") @@ -322,6 +328,9 @@ func makeOptions(daemonConfig *daemonConfig) (options api.ExtraOptions) { options.ReadTimeout = daemonConfig.readTimeout options.MaxAPIResourcesPerAccount = uint64(daemonConfig.maxAPIResourcesPerAccount) + options.MaxAccountListSize = uint64(daemonConfig.maxAccountListSize) + options.MaxBlocksLimit = uint64(daemonConfig.maxBlocksLimit) + options.DefaultBlocksLimit = uint64(daemonConfig.defaultBlocksLimit) options.MaxTransactionsLimit = uint64(daemonConfig.maxTransactionsLimit) options.DefaultTransactionsLimit = uint64(daemonConfig.defaultTransactionsLimit) options.MaxAccountsLimit = uint64(daemonConfig.maxAccountsLimit) diff --git a/idb/dummy/dummy.go b/idb/dummy/dummy.go index 19e276e8b..459e9d8db 100644 --- a/idb/dummy/dummy.go +++ b/idb/dummy/dummy.go @@ -53,6 +53,11 @@ func (db *dummyIndexerDb) GetBlock(ctx context.Context, round uint64, options id return sdk.BlockHeader{}, nil, nil } +// Blocks is part of idb.IndexerDB +func (db *dummyIndexerDb) BlockHeaders(ctx context.Context, bf idb.BlockHeaderFilter) (<-chan idb.BlockRow, uint64) { + return nil, 0 +} + // Transactions is part of idb.IndexerDB func (db *dummyIndexerDb) Transactions(ctx context.Context, tf idb.TransactionFilter) (<-chan idb.TxnRow, uint64) { return nil, 0 diff --git a/idb/idb.go b/idb/idb.go index a4c58c948..3381045ac 100644 --- a/idb/idb.go +++ b/idb/idb.go @@ -15,6 +15,23 @@ import ( sdk "github.com/algorand/go-algorand-sdk/v2/types" ) +// BlockRow is metadata relating to one block in a block query. +type BlockRow struct { + BlockHeader sdk.BlockHeader + + // Error indicates that there was an internal problem processing the expected block. + Error error +} + +// Next returns what should be an opaque string to be used with the next query to resume where a previous limit left off. +func (br BlockRow) Next() (string, error) { + + var b [8]byte + binary.LittleEndian.PutUint64(b[:8], uint64(br.BlockHeader.Round)) + + return base64.URLEncoding.EncodeToString(b[:]), nil +} + // TxnRow is metadata relating to one transaction in a transaction query. type TxnRow struct { // Round is the round where the transaction was committed. @@ -99,6 +116,21 @@ func DecodeTxnRowNext(s string) (uint64 /*round*/, uint32 /*intra*/, error) { return round, intra, nil } +// DecodeBlockRowNext unpacks opaque string returned from BlockRow.Next() +func DecodeBlockRowNext(s string) (uint64 /*round*/, error) { + b, err := base64.URLEncoding.DecodeString(s) + if err != nil { + return 0, fmt.Errorf("DecodeBlockRowNext() decode err: %w", err) + } + + if len(b) != 8 { + return 0, fmt.Errorf("DecodeBlockRowNext() bad next token b: %x", b) + } + + round := binary.LittleEndian.Uint64(b[:8]) + return round, nil +} + // OptionalUint wraps bool and uint. It has a custom marshaller below. type OptionalUint struct { Present bool @@ -173,6 +205,7 @@ type IndexerDb interface { // The next multiple functions return a channel with results as well as the latest round // accounted. + BlockHeaders(ctx context.Context, bf BlockHeaderFilter) (<-chan BlockRow, uint64) Transactions(ctx context.Context, tf TransactionFilter) (<-chan TxnRow, uint64) GetAccounts(ctx context.Context, opts AccountQueryOptions) (<-chan AccountRow, uint64) Assets(ctx context.Context, filter AssetsQuery) (<-chan AssetRow, uint64) @@ -195,6 +228,18 @@ type GetBlockOptions struct { MaxTransactionsLimit uint64 } +// BlockHeaderFilter is a parameter object with all the block filter options. +type BlockHeaderFilter struct { + Limit uint64 + MaxRound *uint64 + MinRound *uint64 + AfterTime time.Time + BeforeTime time.Time + Proposers map[sdk.Address]struct{} + ExpiredParticipationAccounts map[sdk.Address]struct{} + AbsentParticipationAccounts map[sdk.Address]struct{} +} + // TransactionFilter is a parameter object with all the transaction filter options. type TransactionFilter struct { // SkipOptimization is used for testing to ensure the parameters are not modified. diff --git a/idb/mocks/IndexerDb.go b/idb/mocks/IndexerDb.go index 15b508e10..f7b03c408 100644 --- a/idb/mocks/IndexerDb.go +++ b/idb/mocks/IndexerDb.go @@ -186,6 +186,36 @@ func (_m *IndexerDb) Assets(ctx context.Context, filter idb.AssetsQuery) (<-chan return r0, r1 } +// BlockHeaders provides a mock function with given fields: ctx, bf +func (_m *IndexerDb) BlockHeaders(ctx context.Context, bf idb.BlockHeaderFilter) (<-chan idb.BlockRow, uint64) { + ret := _m.Called(ctx, bf) + + if len(ret) == 0 { + panic("no return value specified for BlockHeaders") + } + + var r0 <-chan idb.BlockRow + var r1 uint64 + if rf, ok := ret.Get(0).(func(context.Context, idb.BlockHeaderFilter) (<-chan idb.BlockRow, uint64)); ok { + return rf(ctx, bf) + } + if rf, ok := ret.Get(0).(func(context.Context, idb.BlockHeaderFilter) <-chan idb.BlockRow); ok { + r0 = rf(ctx, bf) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan idb.BlockRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, idb.BlockHeaderFilter) uint64); ok { + r1 = rf(ctx, bf) + } else { + r1 = ret.Get(1).(uint64) + } + + return r0, r1 +} + // Close provides a mock function with given fields: func (_m *IndexerDb) Close() { _m.Called() diff --git a/idb/postgres/postgres.go b/idb/postgres/postgres.go index 1a6850ccb..7e41128eb 100644 --- a/idb/postgres/postgres.go +++ b/idb/postgres/postgres.go @@ -949,6 +949,185 @@ finish: } } +func buildBlockHeadersQuery(bf idb.BlockHeaderFilter) (query string, err error) { + + // Build the terms for the WHERE clause based on the input parameters + var whereTerms []string + { + // Round-based filters + if bf.MaxRound != nil { + whereTerms = append( + whereTerms, + fmt.Sprintf("bh.round <= %d", *bf.MaxRound), + ) + } + if bf.MinRound != nil { + whereTerms = append( + whereTerms, + fmt.Sprintf("bh.round >= %d", *bf.MinRound), + ) + } + + // Timestamp-based filters + // + // Converting the timestamp into a round usually results in faster execution plans + // (compared to the execution plans that would result from using the `block_header.realtime` column directly) + if !bf.AfterTime.IsZero() { + tmpl := `bh.round >= ( + SELECT tmp.round + FROM block_header tmp + WHERE tmp.realtime > (to_timestamp(%d) AT TIME ZONE 'UTC') + ORDER BY tmp.realtime ASC, tmp.round ASC + LIMIT 1 + )` + whereTerms = append( + whereTerms, + fmt.Sprintf(tmpl, bf.AfterTime.UTC().Unix()), + ) + } + if !bf.BeforeTime.IsZero() { + tmpl := `bh.round <= ( + SELECT tmp.round + FROM block_header tmp + WHERE tmp.realtime < (to_timestamp(%d) AT TIME ZONE 'UTC') + ORDER BY tmp.realtime DESC, tmp.round DESC + LIMIT 1 + )` + whereTerms = append( + whereTerms, + fmt.Sprintf(tmpl, bf.BeforeTime.UTC().Unix()), + ) + } + + // Participation-based filters + if len(bf.Proposers) > 0 { + var ps []string + for addr := range bf.Proposers { + ps = append(ps, `'"`+addr.String()+`"'`) + } + whereTerms = append( + whereTerms, + fmt.Sprintf("( (bh.header->'prp') IS NOT NULL AND ((bh.header->'prp')::TEXT IN (%s)) )", strings.Join(ps, ",")), + ) + } + if len(bf.ExpiredParticipationAccounts) > 0 { + var es []string + for addr := range bf.ExpiredParticipationAccounts { + es = append(es, `'`+addr.String()+`'`) + } + whereTerms = append( + whereTerms, + fmt.Sprintf("( (bh.header->'partupdrmv') IS NOT NULL AND (bh.header->'partupdrmv') ?| array[%s] )", strings.Join(es, ",")), + ) + } + if len(bf.AbsentParticipationAccounts) > 0 { + var as []string + for addr := range bf.AbsentParticipationAccounts { + as = append(as, `'`+addr.String()+`'`) + } + whereTerms = append( + whereTerms, + fmt.Sprintf("( (bh.header->'partupdabs') IS NOT NULL AND (bh.header->'partupdabs') ?| array[%s] )", strings.Join(as, ",")), + ) + } + + } + + // Build the full SQL query + var whereClause string + if len(whereTerms) > 0 { + whereClause = "WHERE " + strings.Join(whereTerms, " AND ") + } + tmpl := ` + SELECT bh.header + FROM block_header bh + %s + ORDER BY bh.round ASC + LIMIT %d` + query = fmt.Sprintf(tmpl, whereClause, bf.Limit) + + return query, nil +} + +// This function blocks. `tx` must be non-nil. +func (db *IndexerDb) yieldBlockHeaders(ctx context.Context, tx pgx.Tx, bf idb.BlockHeaderFilter, out chan<- idb.BlockRow) { + + query, err := buildBlockHeadersQuery(bf) + if err != nil { + err = fmt.Errorf("block query err %v", err) + out <- idb.BlockRow{Error: err} + return + } + + rows, err := tx.Query(ctx, query) + if err != nil { + err = fmt.Errorf("block query %#v err %v", query, err) + out <- idb.BlockRow{Error: err} + return + } + db.yieldBlocksThreadSimple(rows, out) +} + +// BlockHeaders is part of idb.IndexerDB +func (db *IndexerDb) BlockHeaders(ctx context.Context, bf idb.BlockHeaderFilter) (<-chan idb.BlockRow, uint64) { + out := make(chan idb.BlockRow, 1) + + tx, err := db.db.BeginTx(ctx, readonlyRepeatableRead) + if err != nil { + out <- idb.BlockRow{Error: err} + close(out) + return out, 0 + } + + round, err := db.getMaxRoundAccounted(ctx, tx) + if err != nil { + out <- idb.BlockRow{Error: err} + close(out) + if rerr := tx.Rollback(ctx); rerr != nil { + db.log.Printf("rollback error: %s", rerr) + } + return out, round + } + + go func() { + db.yieldBlockHeaders(ctx, tx, bf, out) + // Because we return a channel into a "callWithTimeout" function, + // We need to make sure that rollback is called before close() + // otherwise we can end up with a situation where "callWithTimeout" + // will cancel our context, resulting in connection pool churn + if rerr := tx.Rollback(ctx); rerr != nil { + db.log.Printf("rollback error: %s", rerr) + } + close(out) + }() + + return out, round +} + +func (db *IndexerDb) yieldBlocksThreadSimple(rows pgx.Rows, results chan<- idb.BlockRow) { + defer rows.Close() + + for rows.Next() { + var row idb.BlockRow + + var blockheaderjson []byte + err := rows.Scan(&blockheaderjson) + if err != nil { + row.Error = err + } else { + row.BlockHeader, err = encoding.DecodeBlockHeader(blockheaderjson) + if err != nil { + row.Error = fmt.Errorf("failed to decode block header: %w", err) + } + } + + results <- row + } + if err := rows.Err(); err != nil { + results <- idb.BlockRow{Error: err} + } +} + var statusStrings = []string{"Offline", "Online", "NotParticipating"} const offlineStatusIdx = 0