From 084577338ad4882f5797b3e1b30b84718ad40333 Mon Sep 17 00:00:00 2001 From: Gary Malouf <982483+gmalouf@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:04:09 -0500 Subject: [PATCH] Rewind: Re-enable Rewinding (#1645) * Revert "Disable rewinding and reject the query param on account lookups and searches. (#1630)" This reverts commit 249016cf51cf9c3aecf37c6fc8c09774523f02bc. The synthetic transaction implementation of payouts in the indexer should allow balances retrieved via rewind to calculate as before. * GCI lint warning fix. * Add HeartbeatTxn to be ignored during rewinding. --- accounting/rewind.go | 189 +++++++++++++ accounting/rewind_test.go | 80 ++++++ api/error_messages.go | 3 +- api/generated/v2/routes.go | 469 +++++++++++++++++---------------- api/generated/v2/types.go | 4 +- api/handlers.go | 50 +++- api/handlers_test.go | 82 +++--- api/indexer.oas2.json | 8 +- api/indexer.oas3.yml | 4 +- api/server.go | 16 +- cmd/algorand-indexer/daemon.go | 5 +- cmd/idbtest/idbtest.go | 4 + 12 files changed, 605 insertions(+), 309 deletions(-) create mode 100644 accounting/rewind.go create mode 100644 accounting/rewind_test.go diff --git a/accounting/rewind.go b/accounting/rewind.go new file mode 100644 index 00000000..989b0d0b --- /dev/null +++ b/accounting/rewind.go @@ -0,0 +1,189 @@ +package accounting + +import ( + "context" + "fmt" + + models "github.com/algorand/indexer/v3/api/generated/v2" + "github.com/algorand/indexer/v3/idb" + "github.com/algorand/indexer/v3/types" + + sdk "github.com/algorand/go-algorand-sdk/v2/types" +) + +// ConsistencyError is returned when the database returns inconsistent (stale) results. +type ConsistencyError struct { + msg string +} + +func (e ConsistencyError) Error() string { + return e.msg +} + +func assetUpdate(account *models.Account, assetid uint64, add, sub uint64) { + if account.Assets == nil { + account.Assets = new([]models.AssetHolding) + } + assets := *account.Assets + for i, ah := range assets { + if ah.AssetId == assetid { + ah.Amount += add + ah.Amount -= sub + assets[i] = ah + // found and updated asset, done + return + } + } + // add asset to list + assets = append(assets, models.AssetHolding{ + Amount: add - sub, + AssetId: assetid, + //Creator: base32 addr string of asset creator, TODO + //IsFrozen: leave nil? // TODO: on close record frozen state for rewind + }) + *account.Assets = assets +} + +// SpecialAccountRewindError indicates that an attempt was made to rewind one of the special accounts. +type SpecialAccountRewindError struct { + account string +} + +// MakeSpecialAccountRewindError helper to initialize a SpecialAccountRewindError. +func MakeSpecialAccountRewindError(account string) *SpecialAccountRewindError { + return &SpecialAccountRewindError{account: account} +} + +// Error is part of the error interface. +func (sare *SpecialAccountRewindError) Error() string { + return fmt.Sprintf("unable to rewind the %s", sare.account) +} + +var specialAccounts *types.SpecialAddresses + +// AccountAtRound queries the idb.IndexerDb object for transactions and rewinds most fields of the account back to +// their values at the requested round. +// `round` must be <= `account.Round` +func AccountAtRound(ctx context.Context, account models.Account, round uint64, db idb.IndexerDb) (acct models.Account, err error) { + // Make sure special accounts cache has been initialized. + if specialAccounts == nil { + var accounts types.SpecialAddresses + accounts, err = db.GetSpecialAccounts(ctx) + if err != nil { + return models.Account{}, fmt.Errorf("unable to get special accounts: %v", err) + } + specialAccounts = &accounts + } + + acct = account + var addr sdk.Address + addr, err = sdk.DecodeAddress(account.Address) + if err != nil { + return + } + + // ensure that the don't attempt to rewind a special account. + if specialAccounts.FeeSink == addr { + err = MakeSpecialAccountRewindError("FeeSink") + return + } + if specialAccounts.RewardsPool == addr { + err = MakeSpecialAccountRewindError("RewardsPool") + return + } + + // Get transactions and rewind account. + tf := idb.TransactionFilter{ + Address: addr[:], + MinRound: round + 1, + MaxRound: account.Round, + } + ctx2, cf := context.WithCancel(ctx) + // In case of a panic before the next defer, call cf() here. + defer cf() + txns, r := db.Transactions(ctx2, tf) + // In case of an error, make sure the context is cancelled, and the channel is cleaned up. + defer func() { + cf() + for range txns { + } + }() + if r < account.Round { + err = ConsistencyError{fmt.Sprintf("queried round r: %d < account.Round: %d", r, account.Round)} + return + } + txcount := 0 + for txnrow := range txns { + if txnrow.Error != nil { + err = txnrow.Error + return + } + txcount++ + stxn := txnrow.Txn + if stxn == nil { + return models.Account{}, + fmt.Errorf("rewinding past inner transactions is not supported") + } + if addr == stxn.Txn.Sender { + acct.AmountWithoutPendingRewards += uint64(stxn.Txn.Fee) + acct.AmountWithoutPendingRewards -= uint64(stxn.SenderRewards) + } + switch stxn.Txn.Type { + case sdk.PaymentTx: + if addr == stxn.Txn.Sender { + acct.AmountWithoutPendingRewards += uint64(stxn.Txn.Amount) + } + if addr == stxn.Txn.Receiver { + acct.AmountWithoutPendingRewards -= uint64(stxn.Txn.Amount) + acct.AmountWithoutPendingRewards -= uint64(stxn.ReceiverRewards) + } + if addr == stxn.Txn.CloseRemainderTo { + // unwind receiving a close-to + acct.AmountWithoutPendingRewards -= uint64(stxn.ClosingAmount) + acct.AmountWithoutPendingRewards -= uint64(stxn.CloseRewards) + } else if !stxn.Txn.CloseRemainderTo.IsZero() { + // unwind sending a close-to + acct.AmountWithoutPendingRewards += uint64(stxn.ClosingAmount) + } + case sdk.KeyRegistrationTx: + // TODO: keyreg does not rewind. workaround: query for txns on an account with typeenum=2 to find previous values it was set to. + case sdk.AssetConfigTx: + if stxn.Txn.ConfigAsset == 0 { + // create asset, unwind the application of the value + assetUpdate(&acct, txnrow.AssetID, 0, stxn.Txn.AssetParams.Total) + } + case sdk.AssetTransferTx: + if addr == stxn.Txn.AssetSender || addr == stxn.Txn.Sender { + assetUpdate(&acct, uint64(stxn.Txn.XferAsset), stxn.Txn.AssetAmount+txnrow.Extra.AssetCloseAmount, 0) + } + if addr == stxn.Txn.AssetReceiver { + assetUpdate(&acct, uint64(stxn.Txn.XferAsset), 0, stxn.Txn.AssetAmount) + } + if addr == stxn.Txn.AssetCloseTo { + assetUpdate(&acct, uint64(stxn.Txn.XferAsset), 0, txnrow.Extra.AssetCloseAmount) + } + case sdk.AssetFreezeTx: + case sdk.HeartbeatTx: + default: + err = fmt.Errorf("%s[%d,%d]: rewinding past txn type %s is not currently supported", account.Address, txnrow.Round, txnrow.Intra, stxn.Txn.Type) + return + } + } + + acct.Round = round + + // Due to accounts being closed and re-opened, we cannot always rewind Rewards. So clear it out. + acct.Rewards = 0 + + // Computing pending rewards is not supported. + acct.PendingRewards = 0 + acct.Amount = acct.AmountWithoutPendingRewards + + // MinBalance is not supported. + acct.MinBalance = 0 + + // TODO: Clear out the closed-at field as well. Like Rewards we cannot know this value for all accounts. + //acct.ClosedAt = 0 + + return +} diff --git a/accounting/rewind_test.go b/accounting/rewind_test.go new file mode 100644 index 00000000..45719739 --- /dev/null +++ b/accounting/rewind_test.go @@ -0,0 +1,80 @@ +package accounting + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + models "github.com/algorand/indexer/v3/api/generated/v2" + "github.com/algorand/indexer/v3/idb" + "github.com/algorand/indexer/v3/idb/mocks" + "github.com/algorand/indexer/v3/types" + + sdk "github.com/algorand/go-algorand-sdk/v2/types" +) + +func TestBasic(t *testing.T) { + var a sdk.Address + a[0] = 'a' + + account := models.Account{ + Address: a.String(), + Amount: 100, + AmountWithoutPendingRewards: 100, + Round: 8, + } + + txnRow := idb.TxnRow{ + Round: 7, + Txn: &sdk.SignedTxnWithAD{ + SignedTxn: sdk.SignedTxn{ + Txn: sdk.Transaction{ + Type: sdk.PaymentTx, + PaymentTxnFields: sdk.PaymentTxnFields{ + Receiver: a, + Amount: sdk.MicroAlgos(2), + }, + }, + }, + }, + } + + ch := make(chan idb.TxnRow, 1) + ch <- txnRow + close(ch) + var outCh <-chan idb.TxnRow = ch + + db := &mocks.IndexerDb{} + db.On("GetSpecialAccounts", mock.Anything).Return(types.SpecialAddresses{}, nil) + db.On("Transactions", mock.Anything, mock.Anything).Return(outCh, uint64(8)) + + account, err := AccountAtRound(context.Background(), account, 6, db) + assert.NoError(t, err) + + assert.Equal(t, uint64(98), account.Amount) +} + +// Test that when idb.Transactions() returns stale data the first time, we return an error. +func TestStaleTransactions1(t *testing.T) { + var a sdk.Address + a[0] = 'a' + + account := models.Account{ + Address: a.String(), + Round: 8, + } + + ch := make(chan idb.TxnRow) + var outCh <-chan idb.TxnRow = ch + close(ch) + + db := &mocks.IndexerDb{} + db.On("GetSpecialAccounts", mock.Anything).Return(types.SpecialAddresses{}, nil) + db.On("Transactions", mock.Anything, mock.Anything).Return(outCh, uint64(7)).Once() + + account, err := AccountAtRound(context.Background(), account, 6, db) + assert.True(t, errors.As(err, &ConsistencyError{}), "err: %v", err) +} diff --git a/api/error_messages.go b/api/error_messages.go index cd364ab4..a8bb5ff6 100644 --- a/api/error_messages.go +++ b/api/error_messages.go @@ -37,7 +37,8 @@ const ( errMultipleApplications = "multiple applications found for this id, please contact us, this shouldn't happen" ErrMultipleBoxes = "multiple application boxes found for this app id and box name, please contact us, this shouldn't happen" ErrFailedLookingUpBoxes = "failed while looking up application boxes" - errRewindingAccountNotSupported = "rewinding account is no longer supported, please remove the `round=` query parameter and try again" + errMultiAcctRewind = "multiple accounts rewind is not supported by this server" + errRewindingAccount = "error while rewinding account" errLookingUpBlockForRound = "error while looking up block for round" errBlockHeaderSearch = "error while searching for block headers" errTransactionSearch = "error while searching for transaction" diff --git a/api/generated/v2/routes.go b/api/generated/v2/routes.go index d90642f3..70d19215 100644 --- a/api/generated/v2/routes.go +++ b/api/generated/v2/routes.go @@ -1290,241 +1290,242 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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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 6b26b2aa..4e561f33 100644 --- a/api/generated/v2/types.go +++ b/api/generated/v2/types.go @@ -1571,7 +1571,7 @@ type SearchForAccountsParams struct { // AuthAddr Include accounts configured to use this spending key. AuthAddr *string `form:"auth-addr,omitempty" json:"auth-addr,omitempty"` - // Round Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected. + // Round Include results for the specified round. For performance reasons, this parameter may be disabled on some configurations. Using application-id or asset-id filters will return both creator and opt-in accounts. Filtering by include-all will return creator and opt-in accounts for deleted assets and accounts. Non-opt-in managers are not included in the results when asset-id is used. Round *uint64 `form:"round,omitempty" json:"round,omitempty"` // ApplicationId Application ID @@ -1583,7 +1583,7 @@ type SearchForAccountsParamsExclude string // LookupAccountByIDParams defines parameters for LookupAccountByID. type LookupAccountByIDParams struct { - // Round Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected. + // Round Include results for the specified round. Round *uint64 `form:"round,omitempty" json:"round,omitempty"` // IncludeAll Include all items including closed accounts, deleted applications, destroyed assets, opted-out asset holdings, and closed-out application localstates. diff --git a/api/handlers.go b/api/handlers.go index 6449c3d6..3ceb91c8 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -17,6 +17,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/algorand/avm-abi/apps" + "github.com/algorand/indexer/v3/accounting" "github.com/algorand/indexer/v3/api/generated/common" "github.com/algorand/indexer/v3/api/generated/v2" "github.com/algorand/indexer/v3/idb" @@ -28,6 +29,13 @@ import ( // ServerImplementation implements the handler interface used by the generated route definitions. type ServerImplementation struct { + // EnableAddressSearchRoundRewind is allows configuring whether or not the + // 'accounts' endpoint allows specifying a round number. This is done for + // performance reasons, because requesting many accounts at a particular + // round could put a lot of strain on the system (especially if the round + // is from long ago). + EnableAddressSearchRoundRewind bool + db idb.IndexerDb dataError func() error @@ -209,9 +217,8 @@ func (si *ServerImplementation) LookupAccountByID(ctx echo.Context, accountID st if err := si.verifyHandler("LookupAccountByID", ctx); err != nil { return badRequest(ctx, err.Error()) } - // The Round parameter is no longer supported (as it was used to request account rewinding) - if params.Round != nil { - return badRequest(ctx, errRewindingAccountNotSupported) + if params.Round != nil && uint64(*params.Round) > math.MaxInt64 { + return notFound(ctx, errValueExceedingInt64) } addr, err := sdk.DecodeAddress(accountID) @@ -241,7 +248,7 @@ func (si *ServerImplementation) LookupAccountByID(ctx echo.Context, accountID st } } - accounts, round, err := si.fetchAccounts(ctx.Request().Context(), options) + accounts, round, err := si.fetchAccounts(ctx.Request().Context(), options, params.Round) if err != nil { var maxErr idb.MaxAPIResourcesPerAccountError if errors.As(err, &maxErr) { @@ -399,13 +406,13 @@ func (si *ServerImplementation) SearchForAccounts(ctx echo.Context, params gener return badRequest(ctx, err.Error()) } if (params.AssetId != nil && uint64(*params.AssetId) > math.MaxInt64) || - (params.ApplicationId != nil && uint64(*params.ApplicationId) > math.MaxInt64) { + (params.ApplicationId != nil && uint64(*params.ApplicationId) > math.MaxInt64) || + (params.Round != nil && uint64(*params.Round) > math.MaxInt64) { return notFound(ctx, errValueExceedingInt64) } - // The Round parameter is no longer supported (as it was used to request account rewinding) - if params.Round != nil { - return badRequest(ctx, errRewindingAccountNotSupported) + if !si.EnableAddressSearchRoundRewind && params.Round != nil { + return badRequest(ctx, errMultiAcctRewind) } var spendingAddrBytes []byte @@ -458,7 +465,7 @@ func (si *ServerImplementation) SearchForAccounts(ctx echo.Context, params gener options.GreaterThanAddress = addr[:] } - accounts, round, err := si.fetchAccounts(ctx.Request().Context(), options) + accounts, round, err := si.fetchAccounts(ctx.Request().Context(), options, params.Round) if err != nil { var maxErr idb.MaxAPIResourcesPerAccountError if errors.As(err, &maxErr) { @@ -1483,7 +1490,7 @@ func (si *ServerImplementation) fetchBlock(ctx context.Context, round uint64, op // fetchAccounts queries for accounts and converts them into generated.Account // objects, optionally rewinding their value back to a particular round. -func (si *ServerImplementation) fetchAccounts(ctx context.Context, options idb.AccountQueryOptions) ([]generated.Account, uint64 /*round*/, error) { +func (si *ServerImplementation) fetchAccounts(ctx context.Context, options idb.AccountQueryOptions, atRound *uint64) ([]generated.Account, uint64 /*round*/, error) { var round uint64 accounts := make([]generated.Account, 0) err := callWithTimeout(ctx, si.log, si.timeout, func(ctx context.Context) error { @@ -1496,12 +1503,33 @@ func (si *ServerImplementation) fetchAccounts(ctx context.Context, options idb.A } }() + if (atRound != nil) && (*atRound > round) { + return fmt.Errorf("%s: the requested round %d > the current round %d", + errRewindingAccount, *atRound, round) + } + for row := range accountchan { if row.Error != nil { return row.Error } - account := row.Account + // Compute for a given round if requested. + var account generated.Account + if atRound != nil { + acct, err := accounting.AccountAtRound(ctx, row.Account, *atRound, si.db) + if err != nil { + // Ignore the error if this is an account search rewind error + _, isSpecialAccountRewindError := err.(*accounting.SpecialAccountRewindError) + if len(options.EqualToAddress) != 0 || !isSpecialAccountRewindError { + return fmt.Errorf("%s: %v", errRewindingAccount, err) + } + // If we didn't return, continue to the next account + continue + } + account = acct + } else { + account = row.Account + } // match the algod equivalent which includes pending rewards account.Rewards += account.PendingRewards diff --git a/api/handlers_test.go b/api/handlers_test.go index b290835d..111dc8a3 100644 --- a/api/handlers_test.go +++ b/api/handlers_test.go @@ -9,6 +9,7 @@ import ( "math" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -673,6 +674,7 @@ func TestFetchTransactions(t *testing.T) { // Setup the mocked responses mockIndexer := &mocks.IndexerDb{} si := testServerImplementation(mockIndexer) + si.EnableAddressSearchRoundRewind = true si.timeout = 1 * time.Second roundTime := time.Now() @@ -748,9 +750,26 @@ func TestFetchTransactions(t *testing.T) { } } +func TestFetchAccountsRewindRoundTooLarge(t *testing.T) { + ch := make(chan idb.AccountRow) + close(ch) + var outCh <-chan idb.AccountRow = ch + + db := &mocks.IndexerDb{} + db.On("GetAccounts", mock.Anything, mock.Anything).Return(outCh, uint64(7)).Once() + + si := testServerImplementation(db) + si.EnableAddressSearchRoundRewind = true + atRound := uint64(8) + _, _, err := si.fetchAccounts(context.Background(), idb.AccountQueryOptions{}, &atRound) + assert.Error(t, err) + assert.True(t, strings.HasPrefix(err.Error(), errRewindingAccount), err.Error()) +} + func TestLookupApplicationLogsByID(t *testing.T) { mockIndexer := &mocks.IndexerDb{} si := testServerImplementation(mockIndexer) + si.EnableAddressSearchRoundRewind = true txnBytes := loadResourceFileOrPanic("test_resources/app_call_logs.txn") var stxn sdk.SignedTxnWithAD @@ -1111,6 +1130,13 @@ func TestBigNumbers(t *testing.T) { return si.SearchForApplications(ctx, generated.SearchForApplicationsParams{ApplicationId: uint64Ptr(uint64(math.MaxInt64 + 1))}) }, }, + { + name: "SearchForAccountInvalidRound", + errString: errValueExceedingInt64, + callHandler: func(ctx echo.Context, si ServerImplementation) error { + return si.SearchForAccounts(ctx, generated.SearchForAccountsParams{Round: uint64Ptr(uint64(math.MaxInt64 + 1))}) + }, + }, { name: "SearchForAccountInvalidAppID", errString: errValueExceedingInt64, @@ -1125,6 +1151,15 @@ func TestBigNumbers(t *testing.T) { return si.SearchForAccounts(ctx, generated.SearchForAccountsParams{AssetId: uint64Ptr(uint64(math.MaxInt64 + 1))}) }, }, + { + name: "LookupAccountByID", + errString: errValueExceedingInt64, + callHandler: func(ctx echo.Context, si ServerImplementation) error { + return si.LookupAccountByID(ctx, + "PBH2JQNVP5SBXLTOWNHHPGU6FUMBVS4ZDITPK5RA5FG2YIIFS6UYEMFM2Y", + generated.LookupAccountByIDParams{Round: uint64Ptr(uint64(math.MaxInt64 + 1))}) + }, + }, { name: "SearchForAssets", errString: errValueExceedingInt64, @@ -1206,53 +1241,6 @@ func TestBigNumbers(t *testing.T) { } } -func TestRewindRoundParameterRejected(t *testing.T) { - testcases := []struct { - name string - errString string - callHandler func(ctx echo.Context, si ServerImplementation) error - }{ - { - name: "SearchForAccountInvalidRound", - errString: errRewindingAccountNotSupported, - callHandler: func(ctx echo.Context, si ServerImplementation) error { - return si.SearchForAccounts(ctx, generated.SearchForAccountsParams{Round: uint64Ptr(uint64(math.MaxInt64 + 1))}) - }, - }, - { - name: "LookupAccountByID", - errString: errRewindingAccountNotSupported, - callHandler: func(ctx echo.Context, si ServerImplementation) error { - return si.LookupAccountByID(ctx, - "PBH2JQNVP5SBXLTOWNHHPGU6FUMBVS4ZDITPK5RA5FG2YIIFS6UYEMFM2Y", - generated.LookupAccountByIDParams{Round: uint64Ptr(uint64(math.MaxInt64 + 1))}) - }, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - - // Make a mock indexer. - mockIndexer := &mocks.IndexerDb{} - - si := testServerImplementation(mockIndexer) - - // Setup context... - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec1 := httptest.NewRecorder() - c := e.NewContext(req, rec1) - - // call handler - require.NoError(t, tc.callHandler(c, *si)) - assert.Equal(t, http.StatusBadRequest, rec1.Code) - bodyStr := rec1.Body.String() - require.Contains(t, bodyStr, tc.errString) - }) - } -} - func TestFetchBlock(t *testing.T) { testcases := []struct { name string diff --git a/api/indexer.oas2.json b/api/indexer.oas2.json index 17ceb531..4e8ca9ce 100644 --- a/api/indexer.oas2.json +++ b/api/indexer.oas2.json @@ -80,7 +80,8 @@ }, { "type": "integer", - "description": "Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected.", + "description": "Include results for the specified round. For performance reasons, this parameter may be disabled on some configurations. Using application-id or asset-id filters will return both creator and opt-in accounts. Filtering by include-all will return creator and opt-in accounts for deleted assets and accounts. Non-opt-in managers are not included in the results when asset-id is used.", + "name": "round", "in": "query" }, @@ -119,10 +120,7 @@ "$ref": "#/parameters/account-id" }, { - "type": "integer", - "description": "Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected.", - "name": "round", - "in": "query" + "$ref": "#/parameters/round" }, { "$ref": "#/parameters/include-all" diff --git a/api/indexer.oas3.yml b/api/indexer.oas3.yml index 7a94029d..705b2f9c 100644 --- a/api/indexer.oas3.yml +++ b/api/indexer.oas3.yml @@ -2630,7 +2630,7 @@ "x-algorand-format": "Address" }, { - "description": "Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected.", + "description": "Include results for the specified round. For performance reasons, this parameter may be disabled on some configurations. Using application-id or asset-id filters will return both creator and opt-in accounts. Filtering by include-all will return creator and opt-in accounts for deleted assets and accounts. Non-opt-in managers are not included in the results when asset-id is used.", "in": "query", "name": "round", "schema": { @@ -2742,7 +2742,7 @@ } }, { - "description": "Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected.", + "description": "Include results for the specified round.", "in": "query", "name": "round", "schema": { diff --git a/api/server.go b/api/server.go index aadd5842..d6d7ee60 100644 --- a/api/server.go +++ b/api/server.go @@ -23,6 +23,9 @@ type ExtraOptions struct { // Tokens are the access tokens which can access the API. Tokens []string + // DeveloperMode turns on features like AddressSearchRoundRewind + DeveloperMode bool + // Respond to Private Network Access preflight requests sent to the indexer. EnablePrivateNetworkAccessHeader bool @@ -146,12 +149,13 @@ func Serve(ctx context.Context, serveAddr string, db idb.IndexerDb, dataError fu } api := ServerImplementation{ - db: db, - dataError: dataError, - timeout: options.handlerTimeout(), - log: log, - disabledParams: disabledMap, - opts: options, + EnableAddressSearchRoundRewind: options.DeveloperMode, + db: db, + dataError: dataError, + timeout: options.handlerTimeout(), + log: log, + disabledParams: disabledMap, + opts: options, } generated.RegisterHandlers(e, &api, middleware...) diff --git a/cmd/algorand-indexer/daemon.go b/cmd/algorand-indexer/daemon.go index 213f2189..d8823329 100644 --- a/cmd/algorand-indexer/daemon.go +++ b/cmd/algorand-indexer/daemon.go @@ -74,7 +74,7 @@ func DaemonCmd() *cobra.Command { cfg.flags = daemonCmd.Flags() cfg.flags.StringVarP(&cfg.daemonServerAddr, "server", "S", ":8980", "host:port to serve API on (default :8980)") cfg.flags.StringVarP(&cfg.tokenString, "token", "t", "", "an optional auth token, when set REST calls must use this token in a bearer format, or in a 'X-Indexer-API-Token' header") - cfg.flags.BoolVarP(&cfg.developerMode, "dev-mode", "", false, "has no effect currently, reserved for future performance intensive operations") + cfg.flags.BoolVarP(&cfg.developerMode, "dev-mode", "", false, "allow performance intensive operations like searching for accounts at a particular round") cfg.flags.BoolVarP(&cfg.enablePrivateNetworkAccessHeader, "enable-private-network-access-header", "", false, "respond to Private Network Access preflight requests") cfg.flags.StringVarP(&cfg.metricsMode, "metrics-mode", "", "OFF", "configure the /metrics endpoint to [ON, OFF, VERBOSE]") cfg.flags.DurationVarP(&cfg.writeTimeout, "write-timeout", "", 30*time.Second, "set the maximum duration to wait before timing out writes to a http response, breaking connection") @@ -309,6 +309,9 @@ func runDaemon(daemonConfig *daemonConfig) error { // makeOptions converts CLI options to server options func makeOptions(daemonConfig *daemonConfig) (options api.ExtraOptions) { options.EnablePrivateNetworkAccessHeader = daemonConfig.enablePrivateNetworkAccessHeader + + options.DeveloperMode = daemonConfig.developerMode + if daemonConfig.tokenString != "" { options.Tokens = append(options.Tokens, daemonConfig.tokenString) } diff --git a/cmd/idbtest/idbtest.go b/cmd/idbtest/idbtest.go index b2f6c3df..e6a62554 100644 --- a/cmd/idbtest/idbtest.go +++ b/cmd/idbtest/idbtest.go @@ -9,6 +9,7 @@ import ( "os" "time" + "github.com/algorand/indexer/v3/accounting" models "github.com/algorand/indexer/v3/api/generated/v2" "github.com/algorand/indexer/v3/idb" _ "github.com/algorand/indexer/v3/idb/postgres" @@ -159,6 +160,9 @@ func main() { MaxRound: account.Round, } printTxnQuery(db, tf) + raccount, err := accounting.AccountAtRound(context.Background(), account, round, db) + maybeFail(err, "AccountAtRound, %v", err) + fmt.Printf("raccount %s\n", string(ajson.Encode(raccount))) } if txntest {