Skip to content

Commit

Permalink
Implement the endpoint GET /v2/block-headers (#1638)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
agodnic authored Dec 20, 2024
1 parent e6a823d commit a99e6f7
Show file tree
Hide file tree
Showing 16 changed files with 1,554 additions and 451 deletions.
244 changes: 222 additions & 22 deletions api/converter_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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: "",
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions api/error_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading

0 comments on commit a99e6f7

Please sign in to comment.