Skip to content

Commit a99e6f7

Browse files
authored
Implement the endpoint GET /v2/block-headers (#1638)
* Implement `GET /v2/blocks` Currently using the exact same implementation as AlgoNode#10 * Rename function * Rename function * Fix compiler error * Revert f17423b * 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.
1 parent e6a823d commit a99e6f7

File tree

16 files changed

+1554
-451
lines changed

16 files changed

+1554
-451
lines changed

api/converter_utils.go

Lines changed: 222 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/base64"
66
"errors"
77
"fmt"
8+
"slices"
89
"sort"
910
"strconv"
1011
"strings"
@@ -34,19 +35,6 @@ func decodeDigest(str *string, field string, errorArr []string) (string, []strin
3435
return "", errorArr
3536
}
3637

37-
// decodeAddress returns the byte representation of the input string, or appends an error to errorArr
38-
func decodeAddress(str *string, field string, errorArr []string) ([]byte, []string) {
39-
if str != nil {
40-
addr, err := sdk.DecodeAddress(*str)
41-
if err != nil {
42-
return nil, append(errorArr, fmt.Sprintf("%s '%s': %v", errUnableToParseAddress, field, err))
43-
}
44-
return addr[:], errorArr
45-
}
46-
// Pass through
47-
return nil, errorArr
48-
}
49-
5038
// decodeAddress converts the role information into a bitmask, or appends an error to errorArr
5139
func decodeAddressRole(role *string, excludeCloseTo *bool, errorArr []string) (idb.AddressRole, []string) {
5240
// If the string is nil, return early.
@@ -298,6 +286,94 @@ func txnRowToTransaction(row idb.TxnRow) (generated.Transaction, error) {
298286
return txn, nil
299287
}
300288

289+
func hdrRowToBlock(row idb.BlockRow) generated.Block {
290+
291+
rewards := generated.BlockRewards{
292+
FeeSink: row.BlockHeader.FeeSink.String(),
293+
RewardsCalculationRound: uint64(row.BlockHeader.RewardsRecalculationRound),
294+
RewardsLevel: row.BlockHeader.RewardsLevel,
295+
RewardsPool: row.BlockHeader.RewardsPool.String(),
296+
RewardsRate: row.BlockHeader.RewardsRate,
297+
RewardsResidue: row.BlockHeader.RewardsResidue,
298+
}
299+
300+
upgradeState := generated.BlockUpgradeState{
301+
CurrentProtocol: string(row.BlockHeader.CurrentProtocol),
302+
NextProtocol: strPtr(string(row.BlockHeader.NextProtocol)),
303+
NextProtocolApprovals: uint64Ptr(row.BlockHeader.NextProtocolApprovals),
304+
NextProtocolSwitchOn: uint64Ptr(uint64(row.BlockHeader.NextProtocolSwitchOn)),
305+
NextProtocolVoteBefore: uint64Ptr(uint64(row.BlockHeader.NextProtocolVoteBefore)),
306+
}
307+
308+
upgradeVote := generated.BlockUpgradeVote{
309+
UpgradeApprove: boolPtr(row.BlockHeader.UpgradeApprove),
310+
UpgradeDelay: uint64Ptr(uint64(row.BlockHeader.UpgradeDelay)),
311+
UpgradePropose: strPtr(string(row.BlockHeader.UpgradePropose)),
312+
}
313+
314+
var partUpdates *generated.ParticipationUpdates = &generated.ParticipationUpdates{}
315+
if len(row.BlockHeader.ExpiredParticipationAccounts) > 0 {
316+
addrs := make([]string, len(row.BlockHeader.ExpiredParticipationAccounts))
317+
for i := 0; i < len(addrs); i++ {
318+
addrs[i] = row.BlockHeader.ExpiredParticipationAccounts[i].String()
319+
}
320+
partUpdates.ExpiredParticipationAccounts = strArrayPtr(addrs)
321+
}
322+
if len(row.BlockHeader.AbsentParticipationAccounts) > 0 {
323+
addrs := make([]string, len(row.BlockHeader.AbsentParticipationAccounts))
324+
for i := 0; i < len(addrs); i++ {
325+
addrs[i] = row.BlockHeader.AbsentParticipationAccounts[i].String()
326+
}
327+
partUpdates.AbsentParticipationAccounts = strArrayPtr(addrs)
328+
}
329+
if *partUpdates == (generated.ParticipationUpdates{}) {
330+
partUpdates = nil
331+
}
332+
333+
// order these so they're deterministic
334+
orderedTrackingTypes := make([]sdk.StateProofType, len(row.BlockHeader.StateProofTracking))
335+
trackingArray := make([]generated.StateProofTracking, len(row.BlockHeader.StateProofTracking))
336+
elems := 0
337+
for key := range row.BlockHeader.StateProofTracking {
338+
orderedTrackingTypes[elems] = key
339+
elems++
340+
}
341+
slices.Sort(orderedTrackingTypes)
342+
for i := 0; i < len(orderedTrackingTypes); i++ {
343+
stpfTracking := row.BlockHeader.StateProofTracking[orderedTrackingTypes[i]]
344+
thing1 := generated.StateProofTracking{
345+
NextRound: uint64Ptr(uint64(stpfTracking.StateProofNextRound)),
346+
Type: uint64Ptr(uint64(orderedTrackingTypes[i])),
347+
VotersCommitment: byteSliceOmitZeroPtr(stpfTracking.StateProofVotersCommitment),
348+
OnlineTotalWeight: uint64Ptr(uint64(stpfTracking.StateProofOnlineTotalWeight)),
349+
}
350+
trackingArray[orderedTrackingTypes[i]] = thing1
351+
}
352+
353+
ret := generated.Block{
354+
Bonus: uint64PtrOrNil(uint64(row.BlockHeader.Bonus)),
355+
FeesCollected: uint64PtrOrNil(uint64(row.BlockHeader.FeesCollected)),
356+
GenesisHash: row.BlockHeader.GenesisHash[:],
357+
GenesisId: row.BlockHeader.GenesisID,
358+
ParticipationUpdates: partUpdates,
359+
PreviousBlockHash: row.BlockHeader.Branch[:],
360+
Proposer: addrPtr(row.BlockHeader.Proposer),
361+
ProposerPayout: uint64PtrOrNil(uint64(row.BlockHeader.ProposerPayout)),
362+
Rewards: &rewards,
363+
Round: uint64(row.BlockHeader.Round),
364+
Seed: row.BlockHeader.Seed[:],
365+
StateProofTracking: &trackingArray,
366+
Timestamp: uint64(row.BlockHeader.TimeStamp),
367+
Transactions: nil,
368+
TransactionsRoot: row.BlockHeader.TxnCommitments.NativeSha512_256Commitment[:],
369+
TransactionsRootSha256: row.BlockHeader.TxnCommitments.Sha256Commitment[:],
370+
TxnCounter: uint64Ptr(row.BlockHeader.TxnCounter),
371+
UpgradeState: &upgradeState,
372+
UpgradeVote: &upgradeVote,
373+
}
374+
return ret
375+
}
376+
301377
func signedTxnWithAdToTransaction(stxn *sdk.SignedTxnWithAD, extra rowData) (generated.Transaction, error) {
302378
var payment *generated.TransactionPayment
303379
var keyreg *generated.TransactionKeyreg
@@ -640,9 +716,14 @@ func edIndexToAddress(index uint64, txn sdk.Transaction, shared []sdk.Address) (
640716
}
641717

642718
func (si *ServerImplementation) assetParamsToAssetQuery(params generated.SearchForAssetsParams) (idb.AssetsQuery, error) {
643-
creator, errorArr := decodeAddress(params.Creator, "creator", make([]string, 0))
644-
if len(errorArr) != 0 {
645-
return idb.AssetsQuery{}, errors.New(errUnableToParseAddress)
719+
720+
var creatorAddressBytes []byte
721+
if params.Creator != nil {
722+
creator, err := sdk.DecodeAddress(*params.Creator)
723+
if err != nil {
724+
return idb.AssetsQuery{}, fmt.Errorf("unable to parse creator address: %w", err)
725+
}
726+
creatorAddressBytes = creator[:]
646727
}
647728

648729
var assetGreaterThan *uint64
@@ -657,7 +738,7 @@ func (si *ServerImplementation) assetParamsToAssetQuery(params generated.SearchF
657738
query := idb.AssetsQuery{
658739
AssetID: params.AssetId,
659740
AssetIDGreaterThan: assetGreaterThan,
660-
Creator: creator,
741+
Creator: creatorAddressBytes,
661742
Name: strOrDefault(params.Name),
662743
Unit: strOrDefault(params.Unit),
663744
Query: "",
@@ -669,9 +750,14 @@ func (si *ServerImplementation) assetParamsToAssetQuery(params generated.SearchF
669750
}
670751

671752
func (si *ServerImplementation) appParamsToApplicationQuery(params generated.SearchForApplicationsParams) (idb.ApplicationQuery, error) {
672-
addr, errorArr := decodeAddress(params.Creator, "creator", make([]string, 0))
673-
if len(errorArr) != 0 {
674-
return idb.ApplicationQuery{}, errors.New(errUnableToParseAddress)
753+
754+
var creatorAddressBytes []byte
755+
if params.Creator != nil {
756+
addr, err := sdk.DecodeAddress(*params.Creator)
757+
if err != nil {
758+
return idb.ApplicationQuery{}, fmt.Errorf("unable to parse creator address: %w", err)
759+
}
760+
creatorAddressBytes = addr[:]
675761
}
676762

677763
var appGreaterThan *uint64
@@ -686,7 +772,7 @@ func (si *ServerImplementation) appParamsToApplicationQuery(params generated.Sea
686772
return idb.ApplicationQuery{
687773
ApplicationID: params.ApplicationId,
688774
ApplicationIDGreaterThan: appGreaterThan,
689-
Address: addr,
775+
Address: creatorAddressBytes,
690776
IncludeDeleted: boolOrDefault(params.IncludeAll),
691777
Limit: min(uintOrDefaultValue(params.Limit, si.opts.DefaultApplicationsLimit), si.opts.MaxApplicationsLimit),
692778
}, nil
@@ -708,7 +794,15 @@ func (si *ServerImplementation) transactionParamsToTransactionFilter(params gene
708794
filter.NextToken = strOrDefault(params.Next)
709795

710796
// Address
711-
filter.Address, errorArr = decodeAddress(params.Address, "address", errorArr)
797+
if params.Address != nil {
798+
addr, err := sdk.DecodeAddress(*params.Address)
799+
if err != nil {
800+
errorArr = append(errorArr, fmt.Sprintf("%s: %v", errUnableToParseAddress, err))
801+
}
802+
filter.Address = addr[:]
803+
}
804+
805+
// Txid
712806
filter.Txid, errorArr = decodeDigest(params.Txid, "txid", errorArr)
713807

714808
// Byte array
@@ -749,6 +843,112 @@ func (si *ServerImplementation) transactionParamsToTransactionFilter(params gene
749843
return
750844
}
751845

846+
func (si *ServerImplementation) blockParamsToBlockFilter(params generated.SearchForBlockHeadersParams) (filter idb.BlockHeaderFilter, err error) {
847+
848+
var errs []error
849+
850+
// Integer
851+
filter.Limit = min(uintOrDefaultValue(params.Limit, si.opts.DefaultBlocksLimit), si.opts.MaxBlocksLimit)
852+
// If min/max are mixed up
853+
//
854+
// This check is performed here instead of in validateBlockFilter because
855+
// when converting params into a filter, the next token is merged with params.MinRound.
856+
if params.MinRound != nil && params.MaxRound != nil && *params.MinRound > *params.MaxRound {
857+
errs = append(errs, errors.New(errInvalidRoundMinMax))
858+
}
859+
filter.MaxRound = params.MaxRound
860+
filter.MinRound = params.MinRound
861+
862+
// String
863+
if params.Next != nil {
864+
n, err := idb.DecodeBlockRowNext(*params.Next)
865+
if err != nil {
866+
errs = append(errs, fmt.Errorf("%s: %w", errUnableToParseNext, err))
867+
}
868+
// Set the MinRound
869+
if filter.MinRound == nil {
870+
filter.MinRound = uint64Ptr(n + 1)
871+
} else {
872+
filter.MinRound = uint64Ptr(max(*filter.MinRound, n+1))
873+
}
874+
}
875+
876+
// Time
877+
if params.AfterTime != nil {
878+
filter.AfterTime = *params.AfterTime
879+
}
880+
if params.BeforeTime != nil {
881+
filter.BeforeTime = *params.BeforeTime
882+
}
883+
884+
// Address list
885+
{
886+
// Make sure at most one of the participation parameters is set
887+
numParticipationFilters := 0
888+
if params.Proposers != nil {
889+
numParticipationFilters++
890+
}
891+
if params.Expired != nil {
892+
numParticipationFilters++
893+
}
894+
if params.Absent != nil {
895+
numParticipationFilters++
896+
}
897+
if numParticipationFilters > 1 {
898+
errs = append(errs, errors.New("only one of `proposer`, `expired`, or `absent` can be specified"))
899+
}
900+
901+
// Validate the number of items in the participation account lists
902+
if params.Proposers != nil && uint64(len(*params.Proposers)) > si.opts.MaxAccountListSize {
903+
errs = append(errs, fmt.Errorf("proposers list too long, max size is %d", si.opts.MaxAccountListSize))
904+
}
905+
if params.Expired != nil && uint64(len(*params.Expired)) > si.opts.MaxAccountListSize {
906+
errs = append(errs, fmt.Errorf("expired list too long, max size is %d", si.opts.MaxAccountListSize))
907+
}
908+
if params.Absent != nil && uint64(len(*params.Absent)) > si.opts.MaxAccountListSize {
909+
errs = append(errs, fmt.Errorf("absent list too long, max size is %d", si.opts.MaxAccountListSize))
910+
}
911+
912+
filter.Proposers = make(map[sdk.Address]struct{}, 0)
913+
if params.Proposers != nil {
914+
for _, s := range *params.Proposers {
915+
addr, err := sdk.DecodeAddress(s)
916+
if err != nil {
917+
errs = append(errs, fmt.Errorf("unable to parse proposer address `%s`: %w", s, err))
918+
} else {
919+
filter.Proposers[addr] = struct{}{}
920+
}
921+
}
922+
}
923+
924+
filter.ExpiredParticipationAccounts = make(map[sdk.Address]struct{}, 0)
925+
if params.Expired != nil {
926+
for _, s := range *params.Expired {
927+
addr, err := sdk.DecodeAddress(s)
928+
if err != nil {
929+
errs = append(errs, fmt.Errorf("unable to parse expired address `%s`: %w", s, err))
930+
} else {
931+
filter.ExpiredParticipationAccounts[addr] = struct{}{}
932+
}
933+
}
934+
}
935+
936+
filter.AbsentParticipationAccounts = make(map[sdk.Address]struct{}, 0)
937+
if params.Absent != nil {
938+
for _, s := range *params.Absent {
939+
addr, err := sdk.DecodeAddress(s)
940+
if err != nil {
941+
errs = append(errs, fmt.Errorf("unable to parse absent address `%s`: %w", s, err))
942+
} else {
943+
filter.AbsentParticipationAccounts[addr] = struct{}{}
944+
}
945+
}
946+
}
947+
}
948+
949+
return filter, errors.Join(errs...)
950+
}
951+
752952
func (si *ServerImplementation) maxAccountsErrorToAccountsErrorResponse(maxErr idb.MaxAPIResourcesPerAccountError) generated.ErrorResponse {
753953
addr := maxErr.Address.String()
754954
max := uint64(si.opts.MaxAPIResourcesPerAccount)

api/error_messages.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
const (
1212
errInvalidRoundAndMinMax = "cannot specify round and min-round/max-round"
1313
errInvalidRoundMinMax = "min-round must be less than max-round"
14+
errInvalidTimeMinMax = "after-time must be less than before-time"
1415
errUnableToParseAddress = "unable to parse address"
1516
errInvalidCreatorAddress = "found an invalid creator address"
1617
errUnableToParseBase64 = "unable to parse base64 data"
@@ -38,6 +39,7 @@ const (
3839
ErrFailedLookingUpBoxes = "failed while looking up application boxes"
3940
errRewindingAccountNotSupported = "rewinding account is no longer supported, please remove the `round=` query parameter and try again"
4041
errLookingUpBlockForRound = "error while looking up block for round"
42+
errBlockHeaderSearch = "error while searching for block headers"
4143
errTransactionSearch = "error while searching for transaction"
4244
errZeroAddressCloseRemainderToRole = "searching transactions by zero address with close address role is not supported"
4345
errZeroAddressAssetSenderRole = "searching transactions by zero address with asset sender role is not supported"

0 commit comments

Comments
 (0)