diff --git a/.gitignore b/.gitignore index 1bc4b793f..ee90918b7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ tmp/ # Output of the go coverage tool, specifically when used with LiteIDE *.out +# Output of fixtures_test.go +_*.json + # Dependency directories (remove the comment below to include it) # vendor/ @@ -29,6 +32,9 @@ __pycache__ # jetbrains IDE .idea +# VS Code +.vscode + .deb_tmp .tar_tmp *.deb diff --git a/api/README.md b/api/README.md index 3340b7a04..806ef6ec4 100644 --- a/api/README.md +++ b/api/README.md @@ -5,6 +5,7 @@ We are using a documentation driven process. The API is defined using [OpenAPI v2](https://swagger.io/specification/v2/) in **indexer.oas2.yml**. ## Updating REST API + The Makefile will install our fork of **oapi-codegen**, use `make oapi-codegen` to install it directly. 1. Document your changes by editing **indexer.oas2.yml** @@ -20,3 +21,104 @@ Specifically, `uint64` types aren't strictly supported by OpenAPI. So we added a ## Why do we have indexer.oas2.yml and indexer.oas3.yml? We chose to maintain V2 and V3 versions of the spec because OpenAPI v3 doesn't seem to be widely supported. Some tools worked better with V3 and others with V2, so having both available has been useful. To reduce developer burdon, the v2 specfile is automatically converted v3 using [converter.swagger.io](http://converter.swagger.io/). + +# Fixtures Test +## What is a **Fixtures Test**? + +Currently (September 2022) [fixtures_test.go](./fixtures_test.go) is a library that allows testing Indexer's router to verify that endpoints accept parameters and respond as expected, and guard against future regressions. [app_boxes_fixtures_test.go](./app_boxes_fixtures_test.go) is an example _fixtures test_ and is the _creator_ of the fixture [boxes.json](./test_resources/boxes.json). + +A fixtures test + +1. is defined by a go-slice called a _Seed Fixture_ e.g. [var boxSeedFixture](https://github.com/algorand/indexer/blob/b5025ad640fabac0d778b4cac60d558a698ed560/api/app_boxes_fixtures_test.go#L302-L692) which contains request information for making HTTP requests against an Indexer server +2. iterates through the slice, making each of the defined requests and generating a _Live Fixture_ +3. reads a _Saved Fixture_ from a json file e.g. [boxes.json](./test_resources/boxes.json) +4. persists the _Live Fixture_ to a json file not in source control +5. asserts that the _Saved Fixture_ is equal to the _Live Fixture_ + +In reality, because we always want to save the _Live Fixture_ before making assertions that could fail the test and pre-empt saving, steps (3) and (4) happen in the opposite order. + +## What's the purpose of a Fixtures Test? + +A fixtures test should allow one to quickly stand up an end-to-end test to validate that Indexer endpoints are working as expected. After Indexer's state is programmatically set up, it's easy to add new requests and verify that the responses look exactly as expected. Once you're satisfied that the responses are correct, it's easy to _freeze_ the test and guard against future regressions. +## What does a **Fixtures Test Function** Look Like? + +[func TestBoxes](https://github.com/algorand/indexer/blob/b5025ad640fabac0d778b4cac60d558a698ed560/api/app_boxes_fixtures_test.go#L694_L704) shows the basic structure of a fixtures test. + +1. `setupIdbAndReturnShutdownFunc()` is called to set up the Indexer database + * this isn't expected to require modification +2. `setupLiveBoxes()` is used to prepare the local ledger and process blocks in order to bring Indexer into a particular state + * this will always depend on what the test is trying to achieve + * in this case, an app was used to create and modify a set of boxes which are then queried against + * it is conceivable that instead of bringing Indexer into a particular state, the responses from the DB or even the handler may be mocked, so we could have had `setupLiveBoxesMocker()` instead of `setupLiveBoxes()` +3. `setupLiveServerAndReturnShutdownFunc()` is used to bring up an instance of a real Indexer. + * this shouldn't need to be modified; however, if running in parallel and making assertions that conflict with other tests, you may need to localize the variable `fixtestListenAddr` and run on a separate port + * if running a mock server instead, a different setup function would be needed +4. `validateLiveVsSaved()` runs steps (1) through (5) defined in the previous section + * this is designed to be generic and ought not require much modification going forward + + +## Which Endpoints are Currently _Testable_ in a Fixtures Test? + +Endpoints defined in [proverRoutes](https://github.com/algorand/indexer/blob/b955a31b10d8dce7177383895ed8e57206d69f67/api/fixtures_test.go#L232-L263) are testable. + +Currently (September 2022) these are: + +* `/v2/accounts` +* `/v2/applications` +* `/v2/applications/:application-id` +* `/v2/applications/:application-id/box` +* `/v2/applications/:application-id/boxes` + +## How to Introduce a New Fixtures Test for an _Already Testable_ Endpoint? + +To set up a new test for endpoints defined above one needs to: + +### 1. Define a new _Seed Fixture_ + +For example, consider + +```go +var boxSeedFixture = fixture{ + File: "boxes.json", + Owner: "TestBoxes", + Frozen: true, + Cases: []testCase{ + // /v2/accounts - 1 case + { + Name: "What are all the accounts?", + Request: requestInfo{ + Path: "/v2/accounts", + Params: []param{}, + }, + }, + ... +``` + +A seed fixture is a `struct` with fields +* `File` (_required_) - the name in [test_resources](./test_resources/) where the fixture is read from (and written to with an `_` prefix) +* `Owner` (_recommended_) - a name to define which test "owns" the seed +* `Frozen` (_required_) - set _true_ when you need to run assertions of the _Live Fixture_ vs. the _Saved Fixture_. For tests to pass, it needs to be set _true_. +* `Cases` - the slice of `testCase`s. Each of these has the fields: + * `Name` (_required_) - an identifier for the test case + * `Request` (_required_) - a `requestInfo` struct specifying: + * `Path` (_required_) - the path to be queried + * `Params` (_required but may be empty_) - the slice of parameters (strings `name` and `value`) to be appended to the path +### 2. Define a new _Indexer State_ Setup Function + +There are many examples of setting up state that can be emulated. For example: +* [setupLiveBoxes()](https://github.com/algorand/indexer/blob/b5025ad640fabac0d778b4cac60d558a698ed560/api/app_boxes_fixtures_test.go#L43) for application boxes +* [TestApplicationHandlers()](https://github.com/algorand/indexer/blob/3a9095c2b5ee25093708f980445611a03f2cf4e2/api/handlers_e2e_test.go#L93) for applications +* [TestBlockWithTransactions()](https://github.com/algorand/indexer/blob/800cb135a0c6da0109e7282acf85cbe1961930c6/idb/postgres/postgres_integration_test.go#L339) setup state consisting of a set of basic transactions + +## How to Make a _New Endpoint_ Testable by Fixtures Tests? + +There are 2 steps: + +1. Implement a new function _witness generator_ aka [prover function](https://github.com/algorand/indexer/blob/b955a31b10d8dce7177383895ed8e57206d69f67/api/fixtures_test.go#L103) of type `func(responseInfo) (interface{}, *string)` as examplified in [this section](https://github.com/algorand/indexer/blob/b955a31b10d8dce7177383895ed8e57206d69f67/api/fixtures_test.go#L107-L200). Such a function is supposed to parse an Indexer response's body into a generated model. Currently, all provers are boilerplate, and with generics, it's expected that this step will no longer be necessary (this [POC](https://github.com/tzaffi/indexer/blob/generic-boxes/api/fixtures_test.go#L119-L155) shows how it would be done with generics). +2. Define a new route in the [proverRoutes struct](https://github.com/algorand/indexer/blob/b955a31b10d8dce7177383895ed8e57206d69f67/api/fixtures_test.go#L232_L263). This is a tree structure which is traversed by splitting a path using `/` and eventually reaching a leaf which consists of a `prover` as defined in #1. + +For example, to enable the endpoint `GET /v2/applications/{application-id}/logs` for fixtures test, one need only define a `logsProof` witness generator and have it mapped in `proverRoutes` under: + +``` +proverRoutes.parts["v2"].parts["applications"].parts[":application-id"].parts["logs"] = logsProof +``` \ No newline at end of file diff --git a/api/app_boxes_fixtures_test.go b/api/app_boxes_fixtures_test.go new file mode 100644 index 000000000..84e184a86 --- /dev/null +++ b/api/app_boxes_fixtures_test.go @@ -0,0 +1,704 @@ +package api + +import ( + "encoding/base32" + "encoding/base64" + "fmt" + "testing" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/ledger" + "github.com/algorand/go-algorand/rpcs" + "github.com/algorand/indexer/processor" + "github.com/algorand/indexer/util/test" + "github.com/stretchr/testify/require" +) + +func goalEncode(t *testing.T, s string) string { + b1, err := logic.NewAppCallBytes(s) + require.NoError(t, err, s) + b2, err := b1.Raw() + require.NoError(t, err) + return string(b2) +} + +var goalEncodingExamples map[string]string = map[string]string{ + "str": "str", + "string": "string", + "int": "42", + "integer": "100", + "addr": basics.AppIndex(3).Address().String(), + "address": basics.AppIndex(5).Address().String(), + "b32": base32.StdEncoding.EncodeToString([]byte("b32")), + "base32": base32.StdEncoding.EncodeToString([]byte("base32")), + "byte base32": base32.StdEncoding.EncodeToString([]byte("byte base32")), + "b64": base64.StdEncoding.EncodeToString([]byte("b64")), + "base64": base64.StdEncoding.EncodeToString([]byte("base64")), + "byte base64": base64.StdEncoding.EncodeToString([]byte("byte base64")), + "abi": `(uint64,string,bool[]):[399,"pls pass",[true,false]]`, +} + +func setupLiveBoxes(t *testing.T, proc processor.Processor, l *ledger.Ledger) { + deleted := "DELETED" + + firstAppid := basics.AppIndex(1) + secondAppid := basics.AppIndex(3) + thirdAppid := basics.AppIndex(5) + + // ---- ROUND 1: create and fund the box app and another app which won't have boxes ---- // + currentRound := basics.Round(1) + + createTxn, err := test.MakeComplexCreateAppTxn(test.AccountA, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + + payNewAppTxn := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountA, firstAppid.Address(), basics.Address{}, + basics.Address{}) + + createTxn2, err := test.MakeComplexCreateAppTxn(test.AccountB, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + payNewAppTxn2 := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountB, secondAppid.Address(), basics.Address{}, + basics.Address{}) + + createTxn3, err := test.MakeComplexCreateAppTxn(test.AccountC, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + payNewAppTxn3 := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountC, thirdAppid.Address(), basics.Address{}, + basics.Address{}) + + block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &createTxn, &payNewAppTxn, &createTxn2, &payNewAppTxn2, &createTxn3, &payNewAppTxn3) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 1 --> round 2 + blockHdr, err := l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 2: create 8 boxes for appid == 1 ---- // + currentRound = basics.Round(2) + + boxNames := []string{ + "a great box", + "another great box", + "not so great box", + "disappointing box", + "don't box me in this way", + "I will be assimilated", + "I'm destined for deletion", + "box #8", + } + + expectedAppBoxes := map[basics.AppIndex]map[string]string{} + expectedAppBoxes[firstAppid] = map[string]string{} + newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + boxTxns := make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range boxNames { + expectedAppBoxes[firstAppid][logic.MakeBoxKey(firstAppid, boxName)] = newBoxValue + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + } + + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 2 --> round 3 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 3: populate the boxes appropriately ---- // + currentRound = basics.Round(3) + + appBoxesToSet := map[string]string{ + "a great box": "it's a wonderful box", + "another great box": "I'm wonderful too", + "not so great box": "bummer", + "disappointing box": "RUG PULL!!!!", + "don't box me in this way": "non box-conforming", + "I will be assimilated": "THE BORG", + "I'm destined for deletion": "I'm still alive!!!", + "box #8": "eight is beautiful", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(firstAppid, boxName) + expectedAppBoxes[firstAppid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 3 --> round 4 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 4: delete the unhappy boxes ---- // + currentRound = basics.Round(4) + + appBoxesToDelete := []string{ + "not so great box", + "disappointing box", + "I'm destined for deletion", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToDelete { + args := []string{"delete", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(firstAppid, boxName) + expectedAppBoxes[firstAppid][key] = deleted + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 4 --> round 5 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 5: create 4 new boxes, overwriting one of the former boxes ---- // + currentRound = basics.Round(5) + + randBoxName := []byte{0x52, 0xfd, 0xfc, 0x7, 0x21, 0x82, 0x65, 0x4f, 0x16, 0x3f, 0x5f, 0xf, 0x9a, 0x62, 0x1d, 0x72, 0x95, 0x66, 0xc7, 0x4d, 0x10, 0x3, 0x7c, 0x4d, 0x7b, 0xbb, 0x4, 0x7, 0xd1, 0xe2, 0xc6, 0x49, 0x81, 0x85, 0x5a, 0xd8, 0x68, 0x1d, 0xd, 0x86, 0xd1, 0xe9, 0x1e, 0x0, 0x16, 0x79, 0x39, 0xcb, 0x66, 0x94, 0xd2, 0xc4, 0x22, 0xac, 0xd2, 0x8, 0xa0, 0x7, 0x29, 0x39, 0x48, 0x7f, 0x69, 0x99} + appBoxesToCreate := []string{ + "fantabulous", + "disappointing box", // overwriting here + "AVM is the new EVM", + string(randBoxName), + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToCreate { + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(firstAppid, boxName) + expectedAppBoxes[firstAppid][key] = newBoxValue + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 5 --> round 6 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 6: populate the 4 new boxes ---- // + currentRound = basics.Round(6) + + randBoxValue := []byte{0xeb, 0x9d, 0x18, 0xa4, 0x47, 0x84, 0x4, 0x5d, 0x87, 0xf3, 0xc6, 0x7c, 0xf2, 0x27, 0x46, 0xe9, 0x95, 0xaf, 0x5a, 0x25, 0x36, 0x79, 0x51, 0xba} + appBoxesToSet = map[string]string{ + "fantabulous": "Italian food's the best!", // max char's + "disappointing box": "you made it!", + "AVM is the new EVM": "yes we can!", + string(randBoxName): string(randBoxValue), + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(firstAppid, boxName) + expectedAppBoxes[firstAppid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 6 --> round 7 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 7: create GOAL-encoding boxes for appid == 5 ---- // + currentRound = basics.Round(7) + + encodingExamples := make(map[string]string, len(goalEncodingExamples)) + for k, v := range goalEncodingExamples { + encodingExamples[k] = goalEncode(t, k+":"+v) + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + expectedAppBoxes[thirdAppid] = map[string]string{} + for _, boxName := range encodingExamples { + args := []string{"create", boxName} + expectedAppBoxes[thirdAppid][logic.MakeBoxKey(thirdAppid, boxName)] = newBoxValue + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(thirdAppid), test.AccountC, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + } + + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 7 --> round 8 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 8: populate GOAL-encoding boxes for appid == 5 ---- // + currentRound = basics.Round(8) + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, valPrefix := range encodingExamples { + require.LessOrEqual(t, len(valPrefix), 40) + args := []string{"set", valPrefix, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(thirdAppid), test.AccountC, args, []string{valPrefix}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(thirdAppid, valPrefix) + expectedAppBoxes[thirdAppid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // ---- SUMMARY ---- // + + totals := map[basics.AppIndex]map[string]int{} + for appIndex, appBoxes := range expectedAppBoxes { + totals[appIndex] = map[string]int{ + "tBoxes": 0, + "tBoxBytes": 0, + } + for k, v := range appBoxes { + if v != deleted { + totals[appIndex]["tBoxes"]++ + totals[appIndex]["tBoxBytes"] += len(k) + len(v) - 11 + } + } + } + + // This is a manual sanity check only. + // Validations of db and response contents prior to server response are tested elsewhere. + // TODO: consider incorporating such stateful validations here as well. + fmt.Printf("expectedAppBoxes=%+v\n", expectedAppBoxes) + fmt.Printf("expected totals=%+v\n", totals) +} + +var boxSeedFixture = fixture{ + File: "boxes.json", + Owner: "TestBoxes", + Frozen: true, + Cases: []testCase{ + // /v2/accounts - 1 case + { + Name: "What are all the accounts?", + Request: requestInfo{ + Path: "/v2/accounts", + Params: []param{}, + }, + }, + // /v2/applications - 1 case + { + Name: "What are all the apps?", + Request: requestInfo{ + Path: "/v2/applications", + Params: []param{}, + }, + }, + // /v2/applications/:app-id - 4 cases + { + Name: "Lookup non-existing app 1337", + Request: requestInfo{ + Path: "/v2/applications/1337", + Params: []param{}, + }, + }, + { + Name: "Lookup app 3 (funded with no boxes)", + Request: requestInfo{ + Path: "/v2/applications/3", + Params: []param{}, + }, + }, + { + Name: "Lookup app (funded with boxes)", + Request: requestInfo{ + Path: "/v2/applications/1", + Params: []param{}, + }, + }, + { + Name: "Lookup app (funded with encoding test named boxes)", + Request: requestInfo{ + Path: "/v2/applications/5", + Params: []param{}, + }, + }, + // /v2/accounts/:account-id - 1 non-app case and 2 cases using AppIndex.Address() + { + Name: "Creator account - not an app account - no params", + Request: requestInfo{ + Path: "/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + Params: []param{}, + }, + }, + { + Name: "App 3 (as account) totals no boxes - no params", + Request: requestInfo{ + Path: "/v2/accounts/" + basics.AppIndex(3).Address().String(), + Params: []param{}, + }, + }, + { + Name: "App 1 (as account) totals with boxes - no params", + Request: requestInfo{ + Path: "/v2/accounts/" + basics.AppIndex(1).Address().String(), + Params: []param{}, + }, + }, + // /v2/applications/:app-id/boxes - 5 apps with lots of param variations + { + Name: "Boxes of a app with id == math.MaxInt64", + Request: requestInfo{ + Path: "/v2/applications/9223372036854775807/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of a app with id == math.MaxInt64 + 1", + Request: requestInfo{ + Path: "/v2/applications/9223372036854775808/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of a non-existing app 1337", + Request: requestInfo{ + Path: "/v2/applications/1337/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of app 3 with no boxes: no params", + Request: requestInfo{ + Path: "/v2/applications/3/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of app 5 with goal encoded boxes: no params", + Request: requestInfo{ + Path: "/v2/applications/5/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of app 1 with boxes: no params", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - page 1", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - page 2 - b64", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "b64:Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ=="}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - page 3 - b64", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "b64:Ym94ICM4"}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - MISSING b64 prefix", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "Ym94ICM4"}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - goal app arg encoding str", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "str:box #8"}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - page 4 (empty) - b64", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "b64:ZmFudGFidWxvdXM="}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - ERROR because when next param provided -even empty string- it must be goal app arg encoded", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", ""}, + }, + }, + }, + // /v2/applications/:app-id/box?name=... - lots and lots + { + Name: "Boxes (with made up name param) of a app with id == math.MaxInt64", + Request: requestInfo{ + Path: "/v2/applications/9223372036854775807/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + { + Name: "Box (with made up name param) of a app with id == math.MaxInt64 + 1", + Request: requestInfo{ + Path: "/v2/applications/9223372036854775808/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + + { + Name: "A box attempt for a non-existing app 1337", + Request: requestInfo{ + Path: "/v2/applications/1337/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + { + Name: "A box attempt for a non-existing app 1337 - without the required box name param", + Request: requestInfo{ + Path: "/v2/applications/1337/box", + Params: []param{}, + }, + }, + { + Name: "A box attempt for a existing app 3 - without the required box name param", + Request: requestInfo{ + Path: "/v2/applications/3/box", + Params: []param{}, + }, + }, + { + Name: "App 3 box (non-existing)", + Request: requestInfo{ + Path: "/v2/applications/3/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + { + Name: "App 1 box (non-existing)", + Request: requestInfo{ + Path: "/v2/applications/1/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + { + Name: "App 1 box (a great box)", + Request: requestInfo{ + Path: "/v2/applications/1/box", + Params: []param{ + {"name", "string:a great box"}, + }, + }, + }, + { + Name: "App 5 encoding (str:str) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "str:str"}, + }, + }, + }, + { + Name: "App 5 encoding (integer:100) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "integer:100"}, + }, + }, + }, + { + Name: "App 5 encoding (base32:MJQXGZJTGI======) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "base32:MJQXGZJTGI======"}, + }, + }, + }, + { + Name: "App 5 encoding (b64:YjY0) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "b64:YjY0"}, + }, + }, + }, + { + Name: "App 5 encoding (base64:YmFzZTY0) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "base64:YmFzZTY0"}, + }, + }, + }, + { + Name: "App 5 encoding (string:string) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "string:string"}, + }, + }, + }, + { + Name: "App 5 encoding (int:42) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "int:42"}, + }, + }, + }, + { + Name: "App 5 encoding (abi:(uint64,string,bool[]):[399,\"pls pass\",[true,false]]) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "abi:(uint64,string,bool[]):[399,\"pls pass\",[true,false]]"}, + }, + }, + }, + { + Name: "App 5 encoding (addr:LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "addr:LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY"}, + }, + }, + }, + { + Name: "App 5 encoding (address:2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "address:2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY"}, + }, + }, + }, + { + Name: "App 5 encoding (b32:MIZTE===) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "b32:MIZTE==="}, + }, + }, + }, + { + Name: "App 5 encoding (byte base32:MJ4XIZJAMJQXGZJTGI======) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "byte base32:MJ4XIZJAMJQXGZJTGI======"}, + }, + }, + }, + { + Name: "App 5 encoding (byte base64:Ynl0ZSBiYXNlNjQ=) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "byte base64:Ynl0ZSBiYXNlNjQ="}, + }, + }, + }, + { + Name: "App 5 illegal encoding (just a plain string) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "just a plain string"}, + }, + }, + }, + { + Name: "App 1337 non-existing with illegal encoding (just a plain string) - no params", + Request: requestInfo{ + Path: "/v2/applications/1337/box", + Params: []param{ + {"name", "just a plain string"}, + }, + }, + }, + }, +} + +func TestBoxes(t *testing.T) { + db, proc, l, dbShutdown := setupIdbAndReturnShutdownFunc(t) + defer dbShutdown() + + setupLiveBoxes(t, proc, l) + + serverShutdown := setupLiveServerAndReturnShutdownFunc(t, db) + defer serverShutdown() + + validateOrGenerateFixtures(t, db, boxSeedFixture, "TestBoxes") +} diff --git a/api/error_messages.go b/api/error_messages.go index f5230c05f..291ed7031 100644 --- a/api/error_messages.go +++ b/api/error_messages.go @@ -21,8 +21,12 @@ const ( errFailedSearchingAsset = "failed while searching for asset" errFailedSearchingAssetBalances = "failed while searching for asset balances" errFailedSearchingApplication = "failed while searching for application" + errFailedSearchingBoxes = "failed while searching for application boxes" errFailedLookingUpHealth = "failed while getting indexer health" errNoApplicationsFound = "no application found for application-id" + errNoBoxesFound = "no application boxes found" + errWrongAppidFound = "the wrong application-id was found, please contact us, this shouldn't happen" + errWrongBoxFound = "a box with an unexpected name was found, please contact us, this shouldn't happen" ErrNoAccountsFound = "no accounts found for address" errNoAssetsFound = "no assets found for asset-id" errNoTransactionFound = "no transaction found for transaction id" @@ -30,6 +34,8 @@ const ( errMultipleAccounts = "multiple accounts found for this address, please contact us, this shouldn't happen" errMultipleAssets = "multiple assets found for this id, please contact us, this shouldn't happen" 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" errMultiAcctRewind = "multiple accounts rewind is not supported by this server" errRewindingAccount = "error while rewinding account" errLookingUpBlockForRound = "error while looking up block for round" diff --git a/api/fixtures_test.go b/api/fixtures_test.go new file mode 100644 index 000000000..a94ecb11a --- /dev/null +++ b/api/fixtures_test.go @@ -0,0 +1,472 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/ledger" + + "github.com/algorand/indexer/api/generated/v2" + "github.com/algorand/indexer/idb/postgres" + "github.com/algorand/indexer/processor" + "github.com/algorand/indexer/util/test" +) + +/* See the README.md in this directory for more details about Fixtures Tests */ + +const fixtestListenAddr = "localhost:8999" +const fixtestBaseURL = "http://" + fixtestListenAddr +const fixtestMaxStartup time.Duration = 5 * time.Second +const fixturesDirectory = "test_resources/" + +var fixtestServerOpts = ExtraOptions{ + MaxAPIResourcesPerAccount: 1000, + + MaxTransactionsLimit: 10000, + DefaultTransactionsLimit: 1000, + + MaxAccountsLimit: 1000, + DefaultAccountsLimit: 100, + + MaxAssetsLimit: 1000, + DefaultAssetsLimit: 100, + + MaxBalancesLimit: 10000, + DefaultBalancesLimit: 1000, + + MaxApplicationsLimit: 1000, + DefaultApplicationsLimit: 100, + + MaxBoxesLimit: 10000, + DefaultBoxesLimit: 1000, + + DisabledMapConfig: MakeDisabledMapConfig(), +} + +type fixture struct { + File string `json:"file"` + Owner string `json:"owner"` + LastModified string `json:"lastModified"` + Frozen bool `json:"frozen"` + Cases []testCase `json:"cases"` +} +type testCase struct { + Name string `json:"name"` + Request requestInfo `json:"request"` + Response responseInfo `json:"response"` + Witness interface{} `json:"witness"` + WitnessError *string `json:"witnessError"` +} +type requestInfo struct { + Path string `json:"path"` + Params []param `json:"params"` + URL string `json:"url"` + Route string `json:"route"` // `Route` stores the simulated route found in `proverRoutes` +} +type param struct { + Name string `json:"name"` + Value string `json:"value"` +} +type responseInfo struct { + StatusCode int `json:"statusCode"` + Body string `json:"body"` +} +type prover func(responseInfo) (interface{}, *string) + +// ---- BEGIN provers / witness generators ---- // + +func accountsProof(resp responseInfo) (wit interface{}, errStr *string) { + accounts := generated.AccountsResponse{} + errStr = parseForProver(resp, &accounts) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Accounts generated.AccountsResponse `json:"accounts"` + }{ + Type: fmt.Sprintf("%T", accounts), + Accounts: accounts, + } + return +} +func accountInfoProof(resp responseInfo) (wit interface{}, errStr *string) { + account := generated.AccountResponse{} + errStr = parseForProver(resp, &account) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Account generated.AccountResponse `json:"account"` + }{ + Type: fmt.Sprintf("%T", account), + Account: account, + } + return +} + +func appsProof(resp responseInfo) (wit interface{}, errStr *string) { + apps := generated.ApplicationsResponse{} + errStr = parseForProver(resp, &apps) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Apps generated.ApplicationsResponse `json:"apps"` + }{ + Type: fmt.Sprintf("%T", apps), + Apps: apps, + } + return +} + +func appInfoProof(resp responseInfo) (wit interface{}, errStr *string) { + app := generated.ApplicationResponse{} + errStr = parseForProver(resp, &app) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + App generated.ApplicationResponse `json:"app"` + }{ + Type: fmt.Sprintf("%T", app), + App: app, + } + return +} + +func boxProof(resp responseInfo) (wit interface{}, errStr *string) { + box := generated.BoxResponse{} + errStr = parseForProver(resp, &box) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Box generated.BoxResponse `json:"box"` + }{ + Type: fmt.Sprintf("%T", box), + Box: box, + } + return +} + +func boxesProof(resp responseInfo) (wit interface{}, errStr *string) { + boxes := generated.BoxesResponse{} + errStr = parseForProver(resp, &boxes) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Boxes generated.BoxesResponse `json:"boxes"` + }{ + Type: fmt.Sprintf("%T", boxes), + Boxes: boxes, + } + return +} + +func parseForProver(resp responseInfo, reconstructed interface{}) (errStr *string) { + if resp.StatusCode >= 300 { + s := fmt.Sprintf("%d error", resp.StatusCode) + errStr = &s + return + } + err := json.Unmarshal([]byte(resp.Body), reconstructed) + if err != nil { + s := fmt.Sprintf("unmarshal err: %s", err) + errStr = &s + return + } + return nil +} + +// ---- END provers / witness generators ---- // + +func (f *testCase) proverFromEndoint() (string, prover, error) { + path := f.Request.Path + if len(path) == 0 || path[0] != '/' { + return "", nil, fmt.Errorf("invalid endpoint [%s]", path) + } + return getProof(path[1:]) +} + +type proofPath struct { + parts map[string]proofPath + proof prover +} + +var proverRoutes = proofPath{ + parts: map[string]proofPath{ + "v2": { + parts: map[string]proofPath{ + "accounts": { + proof: accountsProof, + parts: map[string]proofPath{ + ":account-id": { + proof: accountInfoProof, + }, + }, + }, + "applications": { + proof: appsProof, + parts: map[string]proofPath{ + ":application-id": { + proof: appInfoProof, + parts: map[string]proofPath{ + "box": { + proof: boxProof, + }, + "boxes": { + proof: boxesProof, + }, + }, + }, + }, + }, + }, + }, + }, +} + +func getProof(path string) (route string, proof prover, err error) { + var impl func(string, []string, proofPath) (string, prover, error) + impl = func(prefix string, suffix []string, node proofPath) (path string, proof prover, err error) { + if len(suffix) == 0 { + return prefix, node.proof, nil + } + part := suffix[0] + next, ok := node.parts[part] + if ok { + return impl(prefix+"/"+part, suffix[1:], next) + } + // look for a wild-card part, e.g. ":application-id" + for routePart, next := range node.parts { + if routePart[0] == ':' { + return impl(prefix+"/"+routePart, suffix[1:], next) + } + } + // no wild-card, so an error + return prefix, nil, fmt.Errorf("<<>>\nfollowing sub-path (%s) cannot find part [%s]", suffix, node, prefix, part) + } + + return impl("", strings.Split(path, "/"), proverRoutes) +} + +// WARNING: receiver should not call l.Close() +func setupIdbAndReturnShutdownFunc(t *testing.T) (db *postgres.IndexerDb, proc processor.Processor, l *ledger.Ledger, shutdown func()) { + db, dbShutdown, proc, l := setupIdb(t, test.MakeGenesis()) + + shutdown = func() { + dbShutdown() + l.Close() + } + + return +} + +func setupLiveServerAndReturnShutdownFunc(t *testing.T, db *postgres.IndexerDb) (shutdown func()) { + serverCtx, shutdown := context.WithCancel(context.Background()) + go Serve(serverCtx, fixtestListenAddr, db, nil, logrus.New(), fixtestServerOpts) + + serverUp := false + for maxWait := fixtestMaxStartup; !serverUp && maxWait > 0; maxWait -= 50 * time.Millisecond { + time.Sleep(50 * time.Millisecond) + _, resp, _, reqErr, bodyErr := getRequest(t, "/health", []param{}) + if reqErr != nil || bodyErr != nil { + t.Log("waiting for server:", reqErr, bodyErr) + continue + } + if resp.StatusCode != http.StatusOK { + t.Log("waiting for server OK:", resp.StatusCode) + continue + } + serverUp = true + } + require.True(t, serverUp, "api.Serve did not start server in time") + + return +} + +func readFixture(t *testing.T, path string, seed *fixture) fixture { + fileBytes, err := ioutil.ReadFile(path + seed.File) + require.NoError(t, err) + + saved := fixture{} + err = json.Unmarshal(fileBytes, &saved) + require.NoError(t, err) + + return saved +} + +func writeFixture(t *testing.T, path string, save fixture) { + fileBytes, err := json.MarshalIndent(save, "", " ") + require.NoError(t, err) + + err = ioutil.WriteFile(path+save.File, fileBytes, 0644) + require.NoError(t, err) +} + +func getRequest(t *testing.T, endpoint string, params []param) (path string, resp *http.Response, body []byte, reqErr, bodyErr error) { + verbose := true + + path = fixtestBaseURL + endpoint + + if len(params) > 0 { + urlValues := url.Values{} + for _, param := range params { + urlValues.Add(param.Name, param.Value) + } + path += "?" + urlValues.Encode() + } + + t.Log("making HTTP request path", path) + resp, reqErr = http.Get(path) + if reqErr != nil { + reqErr = fmt.Errorf("client: error making http request: %w", reqErr) + return + } + require.NoError(t, reqErr) + defer resp.Body.Close() + + body, bodyErr = ioutil.ReadAll(resp.Body) + + if verbose { + fmt.Printf(` +resp=%+v +body=%s +reqErr=%v +bodyErr=%v`, resp, string(body), reqErr, bodyErr) + } + return +} + +func generateLiveFixture(t *testing.T, seed fixture) (live fixture) { + live = fixture{ + File: seed.File, + Owner: seed.Owner, + Frozen: seed.Frozen, + } + + for i, seedCase := range seed.Cases { + msg := fmt.Sprintf("Case %d. seedCase=%+v.", i, seedCase) + liveCase := testCase{ + Name: seedCase.Name, + Request: seedCase.Request, + } + + path, resp, body, reqErr, bodyErr := getRequest(t, seedCase.Request.Path, seedCase.Request.Params) + require.NoError(t, reqErr, msg) + + // not sure about this one!!! + require.NoError(t, bodyErr, msg) + liveCase.Request.URL = path + + liveCase.Response = responseInfo{ + StatusCode: resp.StatusCode, + Body: string(body), + } + msg += fmt.Sprintf(" newResponse=%+v", liveCase.Response) + + route, prove, err := seedCase.proverFromEndoint() + require.NoError(t, err, msg) + require.Positive(t, len(route), msg) + + liveCase.Request.Route = route + + if prove != nil { + witness, errStr := prove(liveCase.Response) + liveCase.Witness = witness + liveCase.WitnessError = errStr + } + live.Cases = append(live.Cases, liveCase) + } + + live.LastModified = time.Now().String() + writeFixture(t, fixturesDirectory+"_", live) + + return +} + +func validateLiveVsSaved(t *testing.T, seed *fixture, live *fixture) { + require.True(t, live.Frozen, "should be frozen for assertions") + saved := readFixture(t, fixturesDirectory, seed) + + require.Equal(t, saved.Owner, live.Owner, "unexpected discrepancy in Owner") + // sanity check: + require.Equal(t, seed.Owner, saved.Owner, "unexpected discrepancy in Owner") + + require.Equal(t, saved.File, live.File, "unexpected discrepancy in File") + // sanity check: + require.Equal(t, seed.File, saved.File, "unexpected discrepancy in File") + + numSeedCases, numSavedCases, numLiveCases := len(seed.Cases), len(saved.Cases), len(live.Cases) + require.Equal(t, numSavedCases, numLiveCases, "numSavedCases=%d but numLiveCases=%d", numSavedCases, numLiveCases) + // sanity check: + require.Equal(t, numSeedCases, numSavedCases, "numSeedCases=%d but numSavedCases=%d", numSeedCases, numSavedCases) + + for i, seedCase := range seed.Cases { + savedCase, liveCase := saved.Cases[i], live.Cases[i] + msg := fmt.Sprintf("(%d)[%s]. discrepency in seed=\n%+v\nsaved=\n%+v\nlive=\n%+v\n", i, seedCase.Name, seedCase, savedCase, liveCase) + + require.Equal(t, savedCase.Name, liveCase.Name, msg) + // sanity check: + require.Equal(t, seedCase.Name, savedCase.Name, msg) + + // only saved vs live: + require.Equal(t, savedCase.Request, liveCase.Request, msg) + require.Equal(t, savedCase.Response, liveCase.Response, msg) + + route, prove, err := savedCase.proverFromEndoint() + require.NoError(t, err, msg) + require.NotNil(t, prove, msg) + require.Equal(t, savedCase.Request.Route, route, msg) + + savedProof, savedErrStr := prove(savedCase.Response) + liveProof, liveErrStr := prove(liveCase.Response) + require.Equal(t, savedProof, liveProof, msg) + require.Equal(t, savedErrStr, liveErrStr, msg) + // sanity check: + require.Equal(t, savedCase.WitnessError, liveCase.WitnessError, msg) + require.Equal(t, savedCase.WitnessError == nil, savedCase.Witness != nil, msg) + } + // and the saved fixture should be frozen as well before release: + require.True(t, saved.Frozen, "Please ensure that the saved fixture is frozen before merging.") +} + +// When the provided seed has `seed.Frozen == false` assertions will be skipped. +// On the other hand, when `seed.Frozen == false` assertions are made: +// * ownerVariable == saved.Owner == live.Owner +// * saved.File == live.File +// * len(saved.Cases) == len(live.Cases) +// * for each savedCase: +// - savedCase.Name == liveCase.Name +// - savedCase.Request == liveCase.Request +// - recalculated savedCase.Witness == recalculated liveCase.Witness +// +// Regardless of `seed.Frozen`, `live` is saved to `fixturesDirectory + "_" + seed.File` +// NOTE: `live.Witness` is always recalculated via `seed.proof(live.Response)` +// NOTE: by design, the function always fails the test in the case that the seed fixture is not frozen +// as a reminder to freeze the test before merging, so that regressions may be detected going forward. +func validateOrGenerateFixtures(t *testing.T, db *postgres.IndexerDb, seed fixture, owner string) { + require.Equal(t, owner, seed.Owner, "mismatch between purported owners of fixture") + + live := generateLiveFixture(t, seed) + + require.True(t, seed.Frozen, "To guard against regressions, please ensure that the seed is frozen before merging.") + validateLiveVsSaved(t, &seed, &live) +} diff --git a/api/generated/common/routes.go b/api/generated/common/routes.go index 17961eaca..72749488a 100644 --- a/api/generated/common/routes.go +++ b/api/generated/common/routes.go @@ -71,178 +71,185 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "fwAAAP//dZh144zcAAA=", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/api/generated/common/types.go b/api/generated/common/types.go index 876ec2430..d96597b08 100644 --- a/api/generated/common/types.go +++ b/api/generated/common/types.go @@ -91,6 +91,12 @@ type Account struct { // The count of all assets that have been opted in, equivalent to the count of AssetHolding objects held by this account. TotalAssetsOptedIn uint64 `json:"total-assets-opted-in"` + // For app-accounts only. The total number of bytes allocated for the keys and values of boxes which belong to the associated application. + TotalBoxBytes uint64 `json:"total-box-bytes"` + + // For app-accounts only. The total number of boxes which belong to the associated application. + TotalBoxes uint64 `json:"total-boxes"` + // The count of all apps (AppParams objects) created by this account. TotalCreatedApps uint64 `json:"total-created-apps"` @@ -414,6 +420,23 @@ type BlockUpgradeVote struct { UpgradePropose *string `json:"upgrade-propose,omitempty"` } +// Box defines model for Box. +type Box struct { + + // \[name\] box name, base64 encoded + Name []byte `json:"name"` + + // \[value\] box value, base64 encoded. + Value []byte `json:"value"` +} + +// BoxDescriptor defines model for BoxDescriptor. +type BoxDescriptor struct { + + // Base64 encoded box name + Name []byte `json:"name"` +} + // EvalDelta defines model for EvalDelta. type EvalDelta struct { @@ -1009,6 +1032,9 @@ type AuthAddr string // BeforeTime defines model for before-time. type BeforeTime time.Time +// BoxName defines model for box-name. +type BoxName string + // CurrencyGreaterThan defines model for currency-greater-than. type CurrencyGreaterThan uint64 @@ -1176,6 +1202,20 @@ type AssetsResponse struct { // BlockResponse defines model for BlockResponse. type BlockResponse Block +// BoxResponse defines model for BoxResponse. +type BoxResponse Box + +// BoxesResponse defines model for BoxesResponse. +type BoxesResponse struct { + + // \[appidx\] application index. + ApplicationId uint64 `json:"application-id"` + Boxes []BoxDescriptor `json:"boxes"` + + // Base64 encoded final box name result. Used for pagination, when making another request provide this token with the next parameter and prepend with "b64:" if keeping the provided encoding. + NextToken *string `json:"next-token,omitempty"` +} + // ErrorResponse defines model for ErrorResponse. type ErrorResponse struct { Data *map[string]interface{} `json:"data,omitempty"` diff --git a/api/generated/v2/routes.go b/api/generated/v2/routes.go index f8de03b23..7afe92527 100644 --- a/api/generated/v2/routes.go +++ b/api/generated/v2/routes.go @@ -44,6 +44,12 @@ type ServerInterface interface { // (GET /v2/applications/{application-id}) LookupApplicationByID(ctx echo.Context, applicationId uint64, params LookupApplicationByIDParams) error + // Get box information for a given application. + // (GET /v2/applications/{application-id}/box) + LookupApplicationBoxByIDAndName(ctx echo.Context, applicationId uint64, params LookupApplicationBoxByIDAndNameParams) error + // Get box names for a given application. + // (GET /v2/applications/{application-id}/boxes) + SearchForApplicationBoxes(ctx echo.Context, applicationId uint64, params SearchForApplicationBoxesParams) error // (GET /v2/applications/{application-id}/logs) LookupApplicationLogsByID(ctx echo.Context, applicationId uint64, params LookupApplicationLogsByIDParams) error @@ -881,6 +887,101 @@ func (w *ServerInterfaceWrapper) LookupApplicationByID(ctx echo.Context) error { return err } +// LookupApplicationBoxByIDAndName converts echo context to params. +func (w *ServerInterfaceWrapper) LookupApplicationBoxByIDAndName(ctx echo.Context) error { + + validQueryParams := map[string]bool{ + "pretty": true, + "name": true, + } + + // Check for unknown query parameters. + for name, _ := range ctx.QueryParams() { + if _, ok := validQueryParams[name]; !ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unknown parameter detected: %s", name)) + } + } + + var err error + // ------------- Path parameter "application-id" ------------- + var applicationId uint64 + + err = runtime.BindStyledParameter("simple", false, "application-id", ctx.Param("application-id"), &applicationId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter application-id: %s", err)) + } + + // Parameter object where we will unmarshal all parameters from the context + var params LookupApplicationBoxByIDAndNameParams + // ------------- Required query parameter "name" ------------- + if paramValue := ctx.QueryParam("name"); paramValue != "" { + + } else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument name is required, but not found")) + } + + err = runtime.BindQueryParameter("form", true, true, "name", ctx.QueryParams(), ¶ms.Name) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter name: %s", err)) + } + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.LookupApplicationBoxByIDAndName(ctx, applicationId, params) + return err +} + +// SearchForApplicationBoxes converts echo context to params. +func (w *ServerInterfaceWrapper) SearchForApplicationBoxes(ctx echo.Context) error { + + validQueryParams := map[string]bool{ + "pretty": true, + "limit": true, + "next": true, + } + + // Check for unknown query parameters. + for name, _ := range ctx.QueryParams() { + if _, ok := validQueryParams[name]; !ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unknown parameter detected: %s", name)) + } + } + + var err error + // ------------- Path parameter "application-id" ------------- + var applicationId uint64 + + err = runtime.BindStyledParameter("simple", false, "application-id", ctx.Param("application-id"), &applicationId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter application-id: %s", err)) + } + + // Parameter object where we will unmarshal all parameters from the context + var params SearchForApplicationBoxesParams + // ------------- Optional query parameter "limit" ------------- + if paramValue := ctx.QueryParam("limit"); paramValue != "" { + + } + + 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" ------------- + if paramValue := ctx.QueryParam("next"); paramValue != "" { + + } + + 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)) + } + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.SearchForApplicationBoxes(ctx, applicationId, params) + return err +} + // LookupApplicationLogsByID converts echo context to params. func (w *ServerInterfaceWrapper) LookupApplicationLogsByID(ctx echo.Context) error { @@ -1734,6 +1835,8 @@ func RegisterHandlers(router interface { router.GET("/v2/accounts/:account-id/transactions", wrapper.LookupAccountTransactions, m...) router.GET("/v2/applications", wrapper.SearchForApplications, m...) router.GET("/v2/applications/:application-id", wrapper.LookupApplicationByID, m...) + router.GET("/v2/applications/:application-id/box", wrapper.LookupApplicationBoxByIDAndName, m...) + router.GET("/v2/applications/:application-id/boxes", wrapper.SearchForApplicationBoxes, m...) router.GET("/v2/applications/:application-id/logs", wrapper.LookupApplicationLogsByID, m...) router.GET("/v2/assets", wrapper.SearchForAssets, m...) router.GET("/v2/assets/:asset-id", wrapper.LookupAssetByID, m...) @@ -1748,213 +1851,226 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+y9e4/btrYo/lUI/w6wk/6smTR94DTAwUF20qDBTrqDTNoD3Kb3blqibXZkUiWpmXF7", - "890vuBZJURIp2/NKsuu/krH45uJ6P/6clXLTSMGE0bMnf84aquiGGabgL1qWshWm4JX9q2K6VLwxXIrZ", - "E/+NaKO4WM3mM25/bahZz+YzQTesa2P7z2eK/d5yxarZE6NaNp/pcs021A5sto1t7Ub68GE+o1WlmNbj", - "Wf8p6i3hoqzbihGjqNC0tJ80ueRmTcyaa+I6Ey6IFIzIJTHrXmOy5Kyu9Ilf9O8tU9to1W7y/BLns6uC", - "1iupqKiKpVQbamZPZk9dvw87P7sZCiVrNt7jM7lZcMH8jljYULgcYiSp2BIarakhdnV2n76hkUQzqso1", - "WUq1Y5u4iHivTLSb2ZNfZpqJiim4uZLxC/jvUjH2BysMVStmZr/OU3e3NEwVhm8SW3vpbk4x3dZGE2gL", - "e1zxCyaI7XVCXrfakAUjVJC3L56Rr7766juCx2hY5QAuu6tu9nhP4RYqapj/vM+lvn3xDOY/cxvctxVt", - "mpqX1O47+Xyedt/Jy+e5zfQHSQAkF4atmMKD15ql3+pT+2ViGt9x1wStWRcWbPIX6168JqUUS75qFass", - "NLaa4dvUDRMVFytyzrbZKwzT3N0LXLClVGxPKMXGtwqm8fwfFU7LVikmym2xUozC01lTMT6St+4o9Fq2", - "dUXW9AL2TTdAA1xfYvviPV/QurVHxEsln9YrqQl1J1ixJW1rQ/zEpBW1xVl2NAeHhGvSKHnBK1bNLRq/", - "XPNyTUqqcQhoRy55XdvjbzWrcsec3t0OMA+d7LqudR6woU/3MLp97TgJdgUPYbz976/cc68qbn+iNeGG", - "bTTRbbkmVLtVrWVtH7uekwiTkVqWtCYVNZRoIy2GWErlSDeij7nr33EjpIQLrMhiO2wpqt7ou/vY82FX", - "TS3tzpa01ix9Xn738SHBLmMiSet65lCv5RjclEX4gTaNLmDHhTbUsLhN09gWQgqWoKThB6oU3dq/tdla", - "dgFwxKy7naKspWaFkTs4Cc8cwIFFtD8+sYP4CvJuzQhMbj8gTwWQLSy6qestMe4CLEAQz0XMCV+SrWzJ", - "JTydmp9Df7cbC9MbYi8frqzH8li+MQfco8NIgPZCyppRAaDteMjC3l+emtUerrG5JVwwQRUI3ZxUrGaw", - "yQ4I4VdtlNzC5i0ozIls7KXL1owfh6jcsPh5+FYAcLLsaryTHZuu+Yab8XZf0yu+aTdEtJsFU/bCPeUz", - "kihmWiXgshUjJdzZovfyG7pimjBLGDny2jCPRVxCGqIYLdd5rIRr2oGINvSqULIV1R4spSFSxSRbN6zk", - "S84qEkbJraWbZtd6uDhsPR2jGy3HD5JdTphlx3IEu0pcq32e9gtcUHSrJ+QnRzvgq5HnTAQSg8iSkUax", - "Cy5bHTpl1ghTTwtzQhpWNIot+dV4kWfuOCyGwDaOwG0cd1VKYSgXrLK0DxYtDUNsk11TNOGhLOSCavbt", - "1zn+qfuq2DnbJpHuEABwO0FmXdsv2Hd6F2GGHY96TzhEGhvD3yTs7QV30KhAtJHgkexXh1TS+oFe/z00", - "BPHcKJ0WN9IU4BievOWOYjDT3Qklmq8KHHH0SvjqnaXFS14Dnf7NPg5/s622dKl/t55ya74S1LSKPXkv", - "vrB/kYKcGSoqqir7ywZ/et3Whp/xlf2pxp9eyRUvz/gqdyh+rUnNAXTb4D92vLSmwFyF7aam8J9TMzTU", - "NjxnW8XsHLRcwj9XSwAkulR/IO8FJNE0y9wCUtLyKynP2yY+0LKnPVpsycvnOWCBIafwIeAO3UihGUDt", - "U2Qk3rrf7E8W5TEBGD3iBU5/0xIkkW7sRsmGKcNZrK2z//0PxZazJ7P/77TT7p1iN33qJuyEP5MjZfiA", - "qXEoDFGXQ2rIDGya1iBpT2GH8Jx/CWsbztldi1z8xkqDB9RfxgO2acz2oV2wW7u+vdPSPa5+z3MbcuZ3", - "eI5I3Asg0uORf9JOemroigvY+JxcrpkgG3pusQIV0qyZIvYumDaezCP6Q8of1IyOV3AM98ks9WISd6pv", - "fKndrb2y7O4ZsLu3ccUD2euAu04t6Xjz4eZHB3ubILC6pbuf1L++f/8LbRpeXb1//2tP4uKiYlfp+7jT", - "y67lqqioodeD0dVz2zUBoJ8yDPV127cFQLcLPAfcwv1S1Ns6rlt+bNfCsUfMmngVN0eqWjPzd1pTUd4K", - "OV24ofa+4ddccFjED6jqOl6zv+ZwlLdxxe50b+Uho9p67yd8vNzUGw7GgBtf7W1d6V4Xec8SIUx5G4f0", - "sQD/CPG3C/F/r2V5fq27nLoqGHXHzN8rJdUtQJHn3we7ns82TGu6Ymn9eHySvuE+R+cXDNfO7BZAi/gD", - "o7VZP1uzOzjMaOwdR/quU5jdwsHe6bOKdHu79h/tagdD3h/2wJcQTaM/9dP7dJBS78j3x+W9Ox1i9P3v", - "WB92yR+8jjhWAic8t5yXJRdoMOBS2JuizhEJTTjvxXvxnC25AIvsk/fC4qHTBdW81KetZsoJAScrSZ4Q", - "N+Rzauh7MZsPCWHOngK+Jm41TbuoeUnO2TZ1C+gEk1a51Cv5/v2vxEhD68jeHLnGOCtfpzAegxxOUFjI", - "kK0pnEtZodglVVVi6TrYGGFk9NGZmnVO3NhoCnUua2789DMY+XlkNE71QN+kE+4wXPT9Vez9/iiNMx7S", - "S4LwRVrNNPnXhja/cGF+JcX79tGjrxh52jSd0vJfnXONXTSYLW5VAwobh/ss2JVRtAB3gOT2DaMN3P6a", - "Ed1uwLOkrgl06/vwKLlSdOM8C4beQRMXgOvYj5ZFO4TNnWGvD/OIGRzfoP0EVwhtyJrVY/+iQ+8rkqKu", - "fV07JLEJ1833738Br0x/M8FPaEW50J4qaL4S9hE4h7cFI6XlAlh1Ql4uCWC1ea+7c7t2GDOgDq7RR428", - "s3sEAzgpqQDftaYCbyEuCBXboclNM2O8nfMtO2fbd5H9/EA7rHO2oTtIYtXa4QJZ7G6YXFJNNhJssCUT", - "pt46/50EaKYX03Jh0JGg5w2WQRrwaiI3LftwYhSScXSLvJZo05BVLRcO0wQQfRJg1PfJI5U3dgH6FhBK", - "UnDqO86lD4KqxEHgQ8z5+h2+UTvejZ7h5PauDXJLrjT4hjHqaASNn8g1IM85ro2X8j9rBlyZVODA1Qcp", - "7Z90CuiDX8p81lBleMmb/bToOPqbXh87yC7SniTmcjmk2SOSmiQh2LhYUJ0m38x+sRDYanRqtHv0iM7P", - "hNwy7OCEgBOKe6qLGvwcg4c83jFV4IDpt40e47mlpd8FU6Ljqfwy+icSM29rqr0vJjgUexSxF5uTAd53", - "9gAAgO27iaA35lu5nbdmFzR3/nn/l5eisriD6b5favBu8WRl7B7s3cgwEsh7wXjXF+/vYv+10N7WNeFL", - "0opzIS8tc3yIR8t8Zjm/Nn1JUgDnZ9/cCo8DG3vwcQv+m46uza7qn8tlzQUjBeHhDAycAfp+y5Kji233", - "Pt0czAoGXxALg3aAvUdIAXe07EbKGgcmP8r4xYrVIYsUjAOOoX5sQDbR3ywt4QGDB7we+tNykYbG0uMF", - "y2H2iCUsDBz2F4wJdMslXMyJlfMuaG25FSOReQmDpN3XH/RYbcfm6Yc5Pj6tfcAdARU7aE9I966zm5hZ", - "9ItOc7ITK57mW1JXoOG8kIvozmrCSX/n1BleIXdWD2DjN1jAUO0ZPAKdyLtTNB1TtA61zzufS0QjaWjP", - "QUzyXjInNtZUBNeqN0OyndRH9FoRbLJw8nXEnqVQsn0VpRSaCd1CZIuRpaxPRooIzWoGnE3R4ySKc7ZN", - "yzAMEOyZ7xYpKcgDvrQixcOIdVFsxbVhveiT4BDb+ftuIWKjocYwZSf63w/++8kvT4v/RYs/HhXf/f+n", - "v/759YeHX4x+fPzhv/7r//Z/+urDfz387/+YZagGKxol5TK/O9Oopd3fWykDVoaOBDr2tnnvO7iQhhXA", - "oBYXtM6429hGLzQIzy+Al00yDL3LJhg8xTOqR5j2nG2LitdtGl7dvP94bqf9MeibdLs4Z1tgCxkt12RB", - "TbkGvrE3vW0zMXVNd274FW74Fb21/e73GmxTO7Gy4NKf4zN5FwNcO4UOEgCYAo7xrWWPdAJBAql/zmq0", - "9OSDevFxVrbhyZSWdfSYKj/2lMAUrSJPlXCk5F76Dk75XYA3HAQocRNFY+nRjvYVcEH7j/QgmuaSBgn+", - "zgXZeHexMOtGSUuz7uMNtjceft/t3Zb7ItzeIXoa5JRGAAYPxw22A7gi1fE4psEyyV79ja8lEhUwZFHE", - "exs/oy5obr+L8SyIi+GTbSClg2nuDABZQpTAvadgkSyV3MDLGzOlEXDyjETeA8GO5AxmdSkixvBikSeE", - "Lu+0oDFa/4Ntf7Zt4VZtb8+Y7vtkOgWFl2Gc2HKzq7mZLSAF+W7EnZCPLrk5sIdkAqiQ7dn2DnwBtVyl", - "9Q31CvgOueoiv2JwWDAr+7ErVramC/ob6BODyvN+ucmh7jQdpROZbTGzxTT/AAflxtpxdW8CnrzLm6NN", - "o+QFrQtn7MrheCUvHI6H5t42ds/sWPqZvfv+6as3bvlgVmFUFUGcye4K2jWfza4sXyJVBsX6yPg1NUGT", - "MKT/ztjFdc9AdgkB1QOJ2XJaDrgQQXfGz+j1OoPZ0vPlB5q/nJ0Wtzhhr2VNMNd2ena01vYttPSC8tor", - "uP1q00QFN9fZyA+mK/EAN7b0Rgb74lYpxeh1p1/HDkwUzzAROb3B+H1NpIuQDnIuCLegLQcA3dCthRtU", - "T45Rkmg3hX10ha55mTaBiIW2ICHQem8bE2icEZPtiJYWp8dqeTSWbab3ULoNFhnNkTxM7/2aO7uFdO5F", - "reC/t4zwigljPyl4i4PnaV+jz81ybREoYePDHC73KATBhIeIPy6bxY02F0a5jhBk5ZrxpO7W3H7C3d1E", - "/ul0xGP+DxYxLfzEjhij5T4PmlIPRUHvTkXPZn2AP1c844jLmPDFco/PoYpWcGcFuMbt7E495gUtl/Uk", - "jS4OkqPiJCo3kp50sVTyD5bWHoLS9XI8fTQx9k4PvrcUNHg3GWmIDzIrXeOqQhqamy4pSM83XtSQdgZj", - "SpeXrruk7KPLse2x0afvCZhB7PD+In8TEFC9NZQKfHDPIL9dT2JKP9vYRfQUx++erVvzWK9BLxe0PE9z", - "z3ZNTzsvq57d1kjiO4cEQ/1bOiGRw1Zo63L1NExtuOmTgU4wuy4njNPuzQN3LC9AVczsunRftZaJYVpx", - "SYXxGZccQnO9NUPLk+11KZU2kEAtucuKlXxD6zRLXMHpv+sxWRVfccyV1GoWZfpxA5FGcmEQiiqum5pu", - "0Y+tO5qXS/JoHmE1dxsVv+CaL2oGLb7EFguqgVnpVFe+i90eE2atofnjPZqvW1EpVpm1S0KlJQnSCmh+", - "gvvEgplLxgR5BO2+/I48AMcRzS/YQ3uKjgWdPfnyO8iOhH88SiN5yHk3hXQrwLoe6afhGDxncAxLPt2o", - "aSyMWUvz+H3iNWHXfd4StHQkYfdb2lBBVyztjrnZsSbsC7cJVqzBuYgK87gBs0W4Sc/PDLX4qVhTvU7z", - "B7gMUsrNhpuNcyTQcmPhqcs0g5P64TApHGL4sC7/Ebx0GpLW692vjgmztaR2Db5UP9IN6x/rnFBNdGvX", - "3OnLHEI8IS7ZUkWkqLeRRhPOxs4FDIplNkHvvCSN4sKAxNyaZfGfpFxTRUuL/k5yyy0W3349XvLfISMV", - "YaKUdn5x2MLv/dwV00xdpI9eZcDes1quL3kgpCg2FqNUDx2W77/KrONQ2ivdY/RhUML00PvyW3aUIgtu", - "bQ/caISpbwR4YmLAG4Ji2M9B8Hjwzu4dMluVBg/a2hv66e0rx2VspGJ9xe/CB4r0+BXFjOLsAhzk05dk", - "x7zhXah6r1u4yeo/rtnfs5wRW+bfckoQwGDP8XHYn+Nt50RsKc/PGWu4WJ0ubB9k1XHUIZO+YoJprvME", - "dLW2kGM/W5IXaURgaLJgtRQrff+Q7heesSuvGOCkl893rXo0cN+PAsM5dupbeq5kP7k+djCXgLKAefOn", - "bNvZ9b7xCStxnbb9xyBvwU17Z0zyW9c271VtaSLG5TxzUTToQtQ35+J+Lyko3ZmokEcEXLqmXGRcrRmr", - "Mm50DGY8k8pwdGRh7CM7xRlFy/OkPu2d/aKDMxy6U0ducXrvyA1Qtb+xfd752VKmSL5h2tBNk+YkQDeO", - "yAYQlz2+0MUKXJqVUlSaaC5KRlgj9XpXSHImlO5KwGQ110hV4wSXpVSYYhDYJiMH4aL7HslkYGx/jYWS", - "0uQWCvxVHNEspSG0NWsmTHAWZ5DzebgTDHcBoQppJmJl8tqSMZ+ckdb1dk64+RuOo5yHJCUbps5rRoxi", - "jFyupWakZvSCdenXYbS/afLuilcakqvX7IqXcqVos+Ylkapi6oS8cAZ0EPSwk5vv0QlxgX7O2f3dlYDt", - "VZKhFBjvE7fpYxaCuSbe8Rx5hOHPkBVbs/qC6RPy7lLiInQXHK0tn9XrsWgNBglVfLlkgD1gOyAfQr/u", - "Q7QmSCQP/uRhWLen+8cBIwgr9Jo+/ubbHKA9/ubbFKyd/fD08TffWlaLCkLbK15zqrZxM9tqThYtr43L", - "pkrJBSuNVLH0y4U2jFYj2ELdiZsFyP2yFaXzxgpd4nT/Zz88/ebLx//n8TffOmVLNIsPhgSOUBAmLriS", - "wn7yeq4AIW7KMBu74tp8BIbCXIkCRLWMPsOg0uxKPMNGxEVA9W2VAxS2QeWJf/g1q1ZMzVGnD8+Db1iX", - "tMCKEVKZTne4ZBgYZOkiF0bJqi0Zhsqf9fBGtCw+WlLIkR05m8Bb9/UWunV6vZ+nyCeEvARZ6xFy/EL2", - "dwhvjF0whYEb3UAPkDhE69KGKvDSAacdt1VWPUyT9rZZKVqx/UzsQKx+wh4hxNuPcCEPG+Bn237IwffY", - "5B7zmebxojAIy6PENDdFcyawRFZAeJsLpnuBNQwUqzGeCdLfQ9v5iP1fMlZoLtI6+iVjQJ5pWbLGQnpc", - "fIoxS2vwpcNbhvBrz7TZyxeGXzCMtJrgMouS1mVbI7c9wUJelrRWfWNfzZZGWtiLa5J0imtu51qA1zTm", - "jcf5lKVhUQ/IO3PB1Na1QBnfp2m370YNPFTGEY1FzS5YWvJmFAMbf5CXZEPFNtyFnaJbxjwKfworRyYY", - "3B/wtn9y6odo+fjOHEBOL9JeReZwq/ieG6a4rHhJuPiNuYceMJaHGKz3IIXhooUyGYp160ZSTyBGcxiH", - "OYYAlcs0YT/0Qx4Eu+zddhUJCv0AAW3oOcNl+2hSx93se6eKaV61GYW7omV/ZYcBo3u8b6lhpypcrb4l", - "uBwgr/DIpx7dEJYHYDO4rfEpZfFUDy/vg6xoiKciDocnfKZdChvfMiNUSyO9XtSncAhjXzCl+964kaaa", - "Xe0Y27bojY+JfZRELdjhsxTe2Upn59siOu5gzvPPGIMN/Znz9kmcYCbrUViAvuSmXBeZACTbFltgANdA", - "hB9PidwFvEK2XLLS7LMGiGTBsifZVeBnu4rnjFYQFtwFJWE40nApD36UxA6tI5ZHaA6CRMfxwCgPD8ha", - "GyBkF/D/LPeE/QsJ/wND/h7PwPM47u7Tynls44Cni0GnZMs0nErwrY7eSCM1rdN2SD9pxWq6nZoSGvQn", - "DTyvN8UizaGWhlmCgr7caSf5aGr3zqYmt02GGw7Pc/wq4noKw5v8/oLWmVipt6xRTINYQ8m775++chbn", - "XMRUmQ3wo8ZF1htKsskwPsxBFkqjCHRqhO+uWFtS255zZEQ/Rvt51Pt6DjC5pHHRgXq/2PGC/uHDNkhD", - "uXOn6MLFxifrQgjHQZ37hH50FzzchAvMg0FSO/mB6vULamXs7ThjnZUEMqkgnMXukCP+8ts0dNolpCcB", - "c6BLMtGX6IOXDXi4ePQtl6NMEwRSTaypE/T9n1YwidJKhO9W3hmKLd1dxHkXxw5OZA2fMSMT8dVFxjed", - "TU9ZLYrgwp2qMjSfufSScU69nXEbXBcbvlKAodOj5tNiRjrxRBwscgaJencOC+dZhwGQ9jY+WHG3vE4k", - "9TOnAPqlqNgVU50i+XW3u0EibZR2Ga2Y0kWn+0njJgT2+9XoYCitnUIbVk0Il8sDnyLaxGtLVfYav77e", - "+KIAqi6KS8ZX6/TBvrnW0Jbq7760i/u/tBSCew1Kyqf2QQJEZhDtskPDk8lfI4wN1j6TsciZNW7/U4kf", - "U8yyXE1muaY6EBD+M3PYw2z3CUSt+aap0W/KoZJRPpaDYsU79+67jxa4bVfrO3eWZtf22bl9H+nrrmV3", - "xplpz+h/imdy09Qszzw36PGGZV9RzIAcW1GBT6+ylmXZqs7mNPR9/pnWHCvPacizJaRsILFWY7iw/4Gw", - "a9ka/D+jyv4H3QT6/0OoivgkO9QM7gUy3fiBfFTVzMo3FWpXXN8UF5V0NRgdSj+3jb9P8F8E1b9grAJv", - "3y4H5iktDZprnBeTYOZSqvMxC8auGnuXg5QTcS2yMTqlyrRNpTYYBxpMvhLzeoV8cuPFSXHBlFOFSpdH", - "DJWeZs24Gic7IW55PRPxDvyaQoXXzJGxl1V6LAElUH7HhKGmIJP1FDKkxHJo5DowdhEq1bYx8hTaQJNT", - "bVRbGo1eQt2co1u3B42+DLtrpgxJtqW0UnM0bxhZKHbBaE5rhxmQfm+ZvWTQ3NvGJAyQuth9keLwjHFs", - "nXfljC3j6PpPS4Mab5cgDepCb2jzC87yKynIW1xxyMdsO5CNXjWHO3LgUMlK2rQ2RVaKcPwbOaO1icm0", - "XZAz+waDeT6RIXKI2QiO+3eo4asbgKDdMKum2OnLa7DTWdwB8wZEjBxO/0ldMIWRfnuDw8++x4f57F73", - "8Ta82DFWiPa33y7iQ4lQQ1qF4b/659Tly6SiItH8msDbSLgOwdNlwqjtdZJv8FWha3nA9s746sx22HGk", - "vtnoTGt5yVRh55244rrv8Y0te/lEQ0J4HA8Np6widjP6egeBAx90Eq7L7rPoxh7YqGldSlH0Zr9frIP4", - "sgDoKkIM747To5v+6TVedj0UawGS2HKxyudJO2fbT0NWTzggju4TLD55ZQnGEgT7ZpRq7tLZlNBm0Gd0", - "dmSRtuIQcJouuf7Eu8pGUmx4qSQF22yXZpSNOFgnTIFrUziNKXtzplg27A07v9s2LPjojZPxb2gTlQOn", - "2jLBJ3epFAr5G1MOZq7UPWRlTTH36JvH6gYQVad7PvmkwPfniDIPTM/T51NuAIAiw1Dszmn/Pz4yoxi7", - "f4e3c7Ytar5khmfCemoIK/wH2xLf7OTWeIpcPpCeQQ0k+xpdhLscJ0Qq/LKCL3EqFYJ4FOL3tP9Lk4oZ", - "pjYWFNfykmzacg28O10xn0wEDCLgaDqYqDe6j6/uJ8Vx8Si6oSUOhDGrNVUrpogLIyWuWGIwsGwoh3fS", - "OQcOI8XAb4SmjF27Upy8xjjWCHeBaTJKdJLIpOKXcc62p2h5g9+vgUjy+VIyC4OsKXe4pBslX4nz9+yA", - "1/Oe0RJLgPRSHoXl36Lx0q7PqRAONF6OMxPtuz3YBzyHVrPxPvd3zo/PNiHidnvb1/I+Pty8wdws9jGY", - "5w24gOjxQKC+BoGlkn99+S+i2JIpUGF98QVM8MUXc9f0X4/7ny3gffFF2t3hvmz1eEZuDDdvEmL6ReYG", - "dksk/BqSlS/Rt8USOSnAq6uuB1ERoiIQCgssCwUncVbLhiVb4wFHlw4pjxRbtTXFaAAuBFO9TvvkskCV", - "gLkSTv0Ff767Eqm2MYsJraPjSBUhi2qDX68636DaDGYSKSFnx3VH7LJ+dCNidoCbjPgCUxOEEWGoJVM3", - "GfOdG2OPwk8roTDFGyrouI9UBaYYb7gPTSF61ReE8jk4QsQL+72ltYvoERA/8w7yUJTnTGCtJ4v5XIU/", - "woRulVMT2rXCeHYpbhgZE3jdNblu1adiqpKKKlEj7JxeXWQy5FTBrpb1qOzlyOk6Bba9FTsn0i+VkH/J", - "NfT59cCdbJc4BmCsNnm7+SCvaux8DjnGfP/M8F0Bga5Afzr7VpdGbUCtMW30g5fPHxJIMZ5L9hwJX7u3", - "Hdcw2G9FGO4+Wssw29ohq1gylvP4H8QekSXLqId3ZcpfXnRJ8qHV0Etz5yr3jE3+gWrIeu+au8CVTzQg", - "ubdI8vJ5kuXoZYc8OJP6fLZSsk0Hd64wY+kgsh4EA2C6UKhHh67Tx998Syq+YtqckP+B9FFIfMf1cfq3", - "SXhXd6dX3ovAwkJKQuSHXLxSNOfaXegofpC7uCUY5v5v+DoJfOcz4EsKc5WKgX054llI44K8IJtehG96", - "nrG3EfnKhVEUkW8hl8tkhsl/wu+dK4LyOFmx8a3vgZXP2Vax6/Iu/4DO6Hk1iXnqi1Ct4nqIp2a54mn1", - "VeL5fPW46F7QCXllexMmllJZSXvTgvWPXUGmKWeEi7lUSL9kukKSkHlJ/MGUBEWCINIZu4dvLBw2BGLR", - "Evh57QIN7RpCKsmgrHxwBtzMHBf5EOXU8VMjrTAc2R97jD9Hp9hYwmMX/T9rXiegoJH2u47XMSdCEiyR", - "HLfEyOcujRiu2UWO9gDpfp95nE63Spv/LSRUmJq8y0LfaSnKNRVdzdfdOcvHMLlfncZRLY/EM7/N3OoT", - "6/y4znFCZiLIhKsgYwUUSOgVNGr3u+CGbjdMmGtivjfYG/0VoOahmpYAVEYC8L13VZA8Z9vCyPTYDI1N", - "yJkHUQt0p4htoz3OM3JPCMPx1XI73hVfkGURli0YeSNzptedOpEu+HCds23nARMX60Kx6RpSFpLFtGb8", - "Hd+wTi5BRi7FAvG9SCKKl2m5FtOmIMr+28R2wjDTUKEzUIF9p2Fib9tvBLaR8XeUCuUaryByTYJw/YnQ", - "im3D+rE/vYKb/Th40BmckOchjwT4/mE4bpdcAvVZQw9BTJoQ8n9y5fVeVHkdNjgRggPcFsv+jhCBa4C8", - "kW0z5pJcE1ouV6Fsd0IR5JtdLZnq2qWUMb7lUv3RNRzrgXyzccX3RCttGjAY5W66c4Rs6HbmmcHZfGa3", - "Zf+xy7b/LtUfMyiFXkMZwWY59oNMP2AHEwXMk4iCnfWl1h4jGV5iB1o7NKCT5adcbB9YjCKqeqh6Mlaq", - "Y8Ld7odntK7fXQnnGzgONZvwxqQNhpu9cl6YAUNbNO5cZr3WymGH2DpDy9KyeFUXFR6t82+aDOsGYKz4", - "uHLAhIfmTgydqOAfYJOqVXbfoLAas6G8JFStWsxQcg/727GDbLUsXrm0ZuOST45lQ7TQKlYRqVy2H750", - "qZxyOcv3rONCG8cz8rJjDbtA9Qykz63wwxqXPViKogze3JZOWgnTSPIevaDfz07IS0wroRitEMEqbliq", - "okhv/5CJ8ZJBpVQP0UW43ahe1Il9Rb2KLRogWzHwqUjUEPpca9TQRreZG8thJeSq+pf0EW7omZ2pc/DB", - "SyqpENJ8Rvd0YI2afmL3OHahaUKxmprZc/+9haAzi7Bh2IyOVirGVyJT3hgAZEk9IdDD60qSgz6WchnJ", - "4ovXIyoR2PHrIVGwvOBgWH2dVoUU9XbKDTyBXsNZZOotI4IL+eh0F++i3S6j7O77bdGjmTfRDgGwPSt7", - "m/u7RkmhG9cRGgzQwxq7+vaCehKVh2JaOBx6F2cWWTknOTNMNV7bjSN+Uqzw9NNjLFFhFvK2ixF6L56S", - "P5iSTlgNQ9kH0enGXSpal0LxJNEplAzQo27DKQ8syYCbn+AOs6VO3r//5YqOuAxY0w34i+tVrdl5xy8y", - "KfHjO/amMpcD/4a1LnDGiYPt4hzHFjFaVYPs4LHfFyKZkN0aT9vVBgBgoZeZNPyTt7mcvM2J8XtJWi69", - "dOjKuifRp5MmMR3OpT9x7JGKpczHBXY1U8ZT7/P4g/PAXqDhJeSbAoefdQI8JioZUfQcfRqK1LnFybC+", - "E+JQiDO0+9+V1+PUS4/NvG3OW49jSLOUCenahja3WidpJ/KIVpz3OWBZj4Mu9ZEjzH68KF0wDNC5NlhW", - "0xsjExzjgVv3o6dvEL4OE97QODG5Xsu2rjA3+QayNXUiZuJyXEGTwBZ2lWbQiwOcLuK4Zh3NEJ81IS/t", - "yLS+pFvt9bQdYOWH86eKGcwTOsI4nRsql9Nno0r0HGclbzgTJrjcxPdiYTyv3UwP7LSkFulgnil+EZQW", - "zhefdiWC+pY3b3hzxU5oRKDn7php3dcW4MBeE23bPPNj+x2FK43o2e4sHqmCUeFId+A8ZxqdRHZOrXgo", - "jsNeiORwmjx2E8PC9hmbjLCN7KW9puq8RwPdY3UDiBVG8PdG7bEYUdz9VG39dI7w2lky3rSLmpdgRQA/", - "8GBXcEEAFXlLRSU35IXPn/Pg57cvHhLFdFsbD2Q+96kFPreSj5twPLvxRi3dzs+iAJqwfS6cQWXFtVEJ", - "veW97wqywu3yN7KNltp0Tkdor8aEcKMYce6wYJoKwYTnbFtUvG6zgGxbnVf9lHy6XUA1Iy4wb+eCmhKc", - "WUZL0BNT73BwsG1q3Cp4Odx0p/s9GNiuezG9WZrB+/nUAGiHJOGtq9PY0xluDkWfrhviTzfT9dhD5A67", - "yIkoVai9T18yYUD4b8RkRVNg6JblPrSrodUxW32P0q6anQiOoZEdYafHaX+8TOltx2fBJFCEh485Ljsh", - "UH9HWzrOCPpXrgpfHTE/y1ZUenCEXTXoCfPrJO/jWB/fZtKSm2MK9uUEenG0/ZWA3dLFoXQh1IOC71AZ", - "DWug/VPUW5cHbpjzvzvKRskLXqXqMNdyxUuNGphDDcavfN8P89mmrQ2/5jivfV+0YKfJIV85UigqqirC", - "qsfffPPld/3sCJ8QuhofUtK7x23LKRmp4WWfjw272wOJ+as8Wckxysra2tSqMz0E29ocajl2znOHmchg", - "IfloeK9ndf4hiy2hEahLy7bXhnc/ze1va6rXHeqM6nFCnVRKHL4aOv1ByNHHKfgfPYriRn4Zg+eRQxzd", - "I/kU3kaMHhEe9kWJryNMMi5X6baIalcLLz4OE866qZnl7TocmM2s468GSb6f84yPy1rH46VPHRpA/S1p", - "ORFMhWqZyY7jAgVBt6prOAePzucsXlcqFd1aMW1XlHa+Watk8pGplJddssFE5uWD7vZscKaDZCVwblkO", - "tzn/SDltpmDg00jskPbDmmaZc+kZyD5xeSE/1TAvVZ57jhKxToF+NsVpX37eP8mJW87QyS3nnaYb75/2", - "zjukucpfPhMCeYng3zk1Ah8rMIWNy3KHtl+XIrx/XjeP0v8AAQJLiQkPhKElCApYAXT21I00cwUnZ2tj", - "Gv3k9PTy8vLET3NSys3pCoKcCiPbcn3qB4LMjb1saq6Lq49jyW69NbzU5Ombl8Akc1MziJeAq4ty2D6Z", - "PT55hNkOmaANnz2ZfXXy6ORLfCJrgItTzCw8e/Lnh/ns9OLxaewbtUrFPZwxqso1grFrewKZ+xiKsy+r", - "0OiFVE/9cM7OBSbi2ZNfkjXcMUqE279/b5naznwZ31jv11lfx/hwd0w96qU0OvyaVmGWAsVI6bn2yLUA", - "vAcIu2CCcITEmm94qN6tGC3Xjk1LrBnaHrjgrloCXbFovSfkJ82iakXyHEKOUL7wAQy+2E7olFmYHSK1", - "rg7HjQPK8dScbAP+n1R4U8sKguzASiYiR+WTXrkPp5v3BbIwwWi5Ja2oLUPp7U1gJtZha1AJBjPclNSd", - "gIvu817SOn8DfpLCrbCwKzzwRlxpVxCGgXtwft2g1nSysoPxeUiWGjuKzH2hbl9KW89JSD86MCnMnaOH", - "HRY/R55I4IKAbiS5DTuX84LWdWqbkXFxuM3vr9w2O+jH3eq2XINL0nChw5VhAk2XnKKr8I9nM3f9IzcR", - "H5oZ3ENCS9E7wD362ONgV00tKzZ7sqS1ZunjYbjJ3tEEjtA74OLZOU+YQVCqRt9bXUTuILNeQK1tIaRI", - "pycdZSk0W0DdlujMDn118Gw+3Sdnp7jRe/Nut5FPhZFdZDmU1LKP0CV0SlKNEBqfx3Y7nWmnP+eW7+mM", - "d2XpyuljtisoOdkwBUOKEqxpGrCFV1UjzHtvqopruqgxBS3ooXquOEAfgA/qe6DFzjdLXsMbgltE2oeJ", - "IoL9UlQWMRVcdISdvIBedujFlkTopTfMxAhwAAEtovEWHniY4UcpCtdpQwVd2TVa0LUUNg6hQZMjniro", - "NmPgnQLJUG3uACiMc9jmmZKhI9bEDL9CZXwo2wDY5vGjR55/dPr1aLTT3zRKgt2AeQf2Q8LhUkjIF+yZ", - "TDUQyjD2bgH5pk3TmrxzzJUpgFsZj/yTdoSioSsunEsZ3OyGniNTj4GRzqPTYyifWcKyQMEc6Zgm92r2", - "UB53fGn/AH5N8vv9lT8Az66HdoNf3+ges/U68nUzBvvwDfdZ9lsHgOiVjvU+Psxn33zuW7BATVcayq2A", - "3DH79cNAmjn907tU8+pDVrR5JeV52wSjSFxOfiThYFv3rv6+BSQxKeEEU4unO4BSoMZCh1HCImfxGRnV", - "soP49X2p0C1izCOffOST74dPvhNSegABvUOCmSZSRxo1+/rR10cy++mQ2RqI3w4yezrCALvorogcPYd4", - "VDaIbuut16D72ChMFDRBnZ82DeSiAK20/pTo9K2LGX9VsnxU9F5L0XvLpHTw3g8QT7tZupd6FFajiK/B", - "wR45giNH8DlyBCG+9KPwAV40+XTo/51YPY80/0jz743mhxe9H6GPy2ce6bun70GJciTqR6L+uRH1RDrp", - "w0i811amlZk3IvnPcOin8dKO8v+RFzjyAncj//cQwKGi/5EhSKR4ObIFR7bg82YLDpf5A0MwsIXeCitw", - "VAIcCf+R8H90JcCR2B+l/yOZ//zJfByZtq9jXT/R0Lte5TvFHNpmFRHs0j42I4msLTHaQeHjgXYR+CPd", - "uJ3IoKgcl51lya8cdvZZoFzJ486HW0jDMBV8dhWQdwUGO9hxHyPoc3774eufyYl9cvN40tvLy546Pb6C", - "OEfvm/+bPTQPiG2XHiS4bfo0/SEuFlLoa74iRcjSYH/Z4E8Q+XvGV/anGn+CnAMYcZ06As1X+TPQ0G2D", - "/9jx9tqke/zRRvrpFhZbx7ynryTN+X6Svq9+Smog8mKJQXHx1BsuisnpQ4NbWcKCLaWLAorWQK92rME3", - "ODRo4k4FGb+zaE8rbhEwFN8mrx2+oYK8ffGMfPXVV98RfPdWsEFwyW0Yh8SSJvHiAt6oqAmf98FCb188", - "gwWcBZfWvVrtvNQAUbe1cxjx09v4Xzje9C8Z9PcxYyNw104D4YRKrPE0zaWESlCTCovbFbT/IgLyfDaU", - "Km5e1HEgKPVPcjDhMQbs30pu3ccuHWe16BtfcoktDjAp372ZF8N0UX7oVakIjw45hhCp2yXZSyJ0bHY9", - "xvuocT5qDo6m5r+iqfnfOpI4OqfTP/vIendEcVSqLqfD7Jqko4lTLPGQZOxki/9yBsM7QzsHIpv7Cxq9", - "oRXpaIL5TFjZERI69eWs98RExLbfAx29kiv9cVDSkdW6HSPNR9bA/0XV4ZAjPOiVRmUkMYuVS/w+LY65", - "AtZdDai7SWZ1Z7QyX7K14dXVoHoy4aJiV5kc+HfJotdyVXj0f3jU6uq57Zqqv/8ZcP6Iqm/AOUzRrGn/", - "v1jxAi2nkonu5bt31EMcieMB1KqnOnMlTe9PabZ7djt6drd0YLi7hflawU1uPvttdv/OrUdvxaO34lHO", - "vE9lF1zy6Z/+ee5WcLlSmrsT5tmG+0uTcbm/o2rrTlVbgOb2xYX3mAMNpjyim6Nm7tPWzA0x5umC1lSU", - "bKdGDllvjVWNfdbky7UEhOLSNwKCmcSofrKjbHSUjY51H45+ePv64d0a03W73EiMPPeS0l5zwY/JZVJU", - "b9GRhqPI9ldiQA6JzOqZJ0AX6/DTVHgWBmVZkoqBWpMy3zE46xicdQzOOgZnHYOzPo41+hhGdQyjOopv", - "/95hVPt4nPgK3lzE9etjlA/kP8uF3LUTymhTz+RmwQXrBCC/g65GmpGu7iu5XFMT6LBvaCTRwctgx74K", - "JesMfQUnHBCKS8Yv4L9LxdgfrDBUWeZ6H3rb241fIFRyieaPS7kctDfLFKPCjfjwNV9MTW0ga5IJqZUI", - "JX4nc8snb2VLLuGx1Pwc+rsyMPbQN8QC8aA0nZHEqDZrnHbdC1jPzkC5+X0YgI4xf8eYv2PM319AG7Ko", - "ZXmuT/+Eqy5Qj7DTiA2dckqMv9uPuxQX+BhxunQUc7yg+1WwTr0i3NwxNOAzhvi9tH2Rs+W+OZiGSj7P", - "Aac5MVeKdcgHB8nrsOxOwfHzqDw8Kg+PysOj8vCoPDxmdjqqJI8qyaNK8qiSPKokjyrJO1dJfkw14t1X", - "izkqKo+KyqPa5qNG2sRXe/qnlYl2x9oQKz7WPQqZ01rGULdPwI0TyvZPr/gZoZDouA56rPs/zmNYyhG9", - "fCpa4Q/zmWbqwr/1VtWzJ7O1MY1+cnrKruimqdlJKTenkPfB9f8z8P1yswFCFX5xI0e/OFT24dcP/y8A", - "AP//EE3YrWRkAQA=", + "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", + "//+D/yVUYnoBAA==", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/api/generated/v2/types.go b/api/generated/v2/types.go index 2f2ae31e8..62a43fda4 100644 --- a/api/generated/v2/types.go +++ b/api/generated/v2/types.go @@ -91,6 +91,12 @@ type Account struct { // The count of all assets that have been opted in, equivalent to the count of AssetHolding objects held by this account. TotalAssetsOptedIn uint64 `json:"total-assets-opted-in"` + // For app-accounts only. The total number of bytes allocated for the keys and values of boxes which belong to the associated application. + TotalBoxBytes uint64 `json:"total-box-bytes"` + + // For app-accounts only. The total number of boxes which belong to the associated application. + TotalBoxes uint64 `json:"total-boxes"` + // The count of all apps (AppParams objects) created by this account. TotalCreatedApps uint64 `json:"total-created-apps"` @@ -414,6 +420,23 @@ type BlockUpgradeVote struct { UpgradePropose *string `json:"upgrade-propose,omitempty"` } +// Box defines model for Box. +type Box struct { + + // \[name\] box name, base64 encoded + Name []byte `json:"name"` + + // \[value\] box value, base64 encoded. + Value []byte `json:"value"` +} + +// BoxDescriptor defines model for BoxDescriptor. +type BoxDescriptor struct { + + // Base64 encoded box name + Name []byte `json:"name"` +} + // EvalDelta defines model for EvalDelta. type EvalDelta struct { @@ -1009,6 +1032,9 @@ type AuthAddr string // BeforeTime defines model for before-time. type BeforeTime time.Time +// BoxName defines model for box-name. +type BoxName string + // CurrencyGreaterThan defines model for currency-greater-than. type CurrencyGreaterThan uint64 @@ -1176,6 +1202,20 @@ type AssetsResponse struct { // BlockResponse defines model for BlockResponse. type BlockResponse Block +// BoxResponse defines model for BoxResponse. +type BoxResponse Box + +// BoxesResponse defines model for BoxesResponse. +type BoxesResponse struct { + + // \[appidx\] application index. + ApplicationId uint64 `json:"application-id"` + Boxes []BoxDescriptor `json:"boxes"` + + // Base64 encoded final box name result. Used for pagination, when making another request provide this token with the next parameter and prepend with "b64:" if keeping the provided encoding. + NextToken *string `json:"next-token,omitempty"` +} + // ErrorResponse defines model for ErrorResponse. type ErrorResponse struct { Data *map[string]interface{} `json:"data,omitempty"` @@ -1397,6 +1437,23 @@ type LookupApplicationByIDParams struct { IncludeAll *bool `json:"include-all,omitempty"` } +// LookupApplicationBoxByIDAndNameParams defines parameters for LookupApplicationBoxByIDAndName. +type LookupApplicationBoxByIDAndNameParams struct { + + // A box name in goal-arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'. + Name string `json:"name"` +} + +// SearchForApplicationBoxesParams defines parameters for SearchForApplicationBoxes. +type SearchForApplicationBoxesParams struct { + + // Maximum number of results to return. There could be additional pages even if the limit is not reached. + Limit *uint64 `json:"limit,omitempty"` + + // The next page of results. Use the next token provided by the previous results. + Next *string `json:"next,omitempty"` +} + // LookupApplicationLogsByIDParams defines parameters for LookupApplicationLogsByID. type LookupApplicationLogsByIDParams struct { diff --git a/api/handlers.go b/api/handlers.go index 6b305a23a..ea098afcb 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "encoding/base64" "errors" "fmt" "math" @@ -15,6 +17,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/protocol" "github.com/algorand/indexer/accounting" @@ -557,6 +560,141 @@ func (si *ServerImplementation) LookupApplicationByID(ctx echo.Context, applicat }) } +// LookupApplicationBoxByIDAndName returns the value of an application's box +// (GET /v2/applications/{application-id}/box) +func (si *ServerImplementation) LookupApplicationBoxByIDAndName(ctx echo.Context, applicationID uint64, params generated.LookupApplicationBoxByIDAndNameParams) error { + if err := si.verifyHandler("LookupApplicationBoxByIDAndName", ctx); err != nil { + return badRequest(ctx, err.Error()) + } + if uint64(applicationID) > math.MaxInt64 { + return notFound(ctx, errValueExceedingInt64) + } + + encodedBoxName := params.Name + boxNameBytes, err := logic.NewAppCallBytes(encodedBoxName) + if err != nil { + return badRequest(ctx, fmt.Sprintf("LookupApplicationBoxByIDAndName received illegal box name (%s): %s", encodedBoxName, err.Error())) + } + boxName, err := boxNameBytes.Raw() + if err != nil { + return badRequest(ctx, err.Error()) + } + + q := idb.ApplicationBoxQuery{ + ApplicationID: applicationID, + BoxName: boxName, + } + appid, boxes, round, err := si.fetchApplicationBoxes(ctx.Request().Context(), q) + + if err != nil { + if err == sql.ErrNoRows { + return notFound(ctx, fmt.Sprintf("%s: round=%d, appid=%d, boxName=%s", errNoApplicationsFound, round, applicationID, encodedBoxName)) + } + // sql.ErrNoRows is the only expected error condition + msg := fmt.Sprintf("%s: round=?=%d, appid=%d, boxName=%s", errFailedLookingUpBoxes, round, applicationID, encodedBoxName) + return indexerError(ctx, fmt.Errorf("%s: %w", msg, err)) + } + + if len(boxes) == 0 { // this is an unexpected situation as should have received a sql.ErrNoRows from fetchApplicationBoxes's err + msg := fmt.Sprintf("%s: round=?=%d, appid=%d, boxName=%s", errFailedLookingUpBoxes, round, applicationID, encodedBoxName) + return indexerError(ctx, fmt.Errorf(msg)) + } + + if appid != generated.ApplicationId(applicationID) { + return indexerError(ctx, fmt.Errorf("%s: round=%d, appid=%d, wrong appid=%d, boxName=%s", errWrongAppidFound, round, applicationID, appid, encodedBoxName)) + } + + if len(boxes) > 1 { + return indexerError(ctx, fmt.Errorf("%s: round=%d, appid=%d, boxName=%s", errMultipleBoxes, round, applicationID, encodedBoxName)) + } + + box := boxes[0] + if len(box.Name) == 0 && len(boxName) > 0 { + return notFound(ctx, fmt.Sprintf("%s: round=%d, appid=%d, boxName=%s", errNoBoxesFound, round, applicationID, encodedBoxName)) + } + + if string(box.Name) != string(boxName) { + return indexerError(ctx, fmt.Errorf("%s: round=%d, appid=%d, boxName=%s", errWrongBoxFound, round, applicationID, encodedBoxName)) + } + + return ctx.JSON(http.StatusOK, generated.BoxResponse(box)) +} + +// SearchForApplicationBoxes returns box names for an app +// (GET /v2/applications/{application-id}/boxes) +func (si *ServerImplementation) SearchForApplicationBoxes(ctx echo.Context, applicationID uint64, params generated.SearchForApplicationBoxesParams) error { + if err := si.verifyHandler("SearchForApplicationBoxes", ctx); err != nil { + return badRequest(ctx, err.Error()) + } + if uint64(applicationID) > math.MaxInt64 { + return notFound(ctx, errValueExceedingInt64) + } + + q := idb.ApplicationBoxQuery{ + ApplicationID: applicationID, + OmitValues: true, + } + if params.Limit != nil { + q.Limit = *params.Limit + } + if params.Next != nil { + encodedBoxName := *params.Next + boxNameBytes, err := logic.NewAppCallBytes(encodedBoxName) + if err != nil { + return badRequest(ctx, fmt.Sprintf("SearchForApplication received illegal next token (%s): %s", encodedBoxName, err.Error())) + } + prevBox, err := boxNameBytes.Raw() + if err != nil { + return badRequest(ctx, err.Error()) + } + q.PrevFinalBox = []byte(prevBox) + } + + appid, boxes, round, err := si.fetchApplicationBoxes(ctx.Request().Context(), q) + + if err != nil { + if err == sql.ErrNoRows { + return notFound(ctx, fmt.Sprintf("%s: round=%d, appid=%d", errNoApplicationsFound, round, applicationID)) + } + // sql.ErrNoRows is the only expected error condition + msg := fmt.Sprintf("%s: round=?=%d, appid=%d", errFailedSearchingBoxes, round, applicationID) + return indexerError(ctx, fmt.Errorf("%s: %w", msg, err)) + } + + if len(boxes) == 0 { // this is an unexpected situation as should have received a sql.ErrNoRows from fetchApplicationBoxes's err + msg := fmt.Sprintf("%s: round=?=%d, appid=%d", errFailedSearchingBoxes, round, applicationID) + return indexerError(ctx, fmt.Errorf(msg)) + } + + if appid != generated.ApplicationId(applicationID) { + return indexerError(ctx, fmt.Errorf("%s: round=%d, appid=%d, wrong appid=%d", errWrongAppidFound, round, applicationID, appid)) + } + + var next *string + finalNameBytes := boxes[len(boxes)-1].Name + if finalNameBytes != nil { + encoded := base64.StdEncoding.EncodeToString(finalNameBytes) + next = strPtr(encoded) + if next != nil { + next = strPtr("b64:" + string(*next)) + } + } + res := generated.BoxesResponse{ + ApplicationId: applicationID, + NextToken: next, + } + descriptors := []generated.BoxDescriptor{} + for _, box := range boxes { + if box.Name == nil { + continue + } + descriptors = append(descriptors, generated.BoxDescriptor{Name: box.Name}) + } + res.Boxes = descriptors + + return ctx.JSON(http.StatusOK, res) +} + // LookupApplicationLogsByID returns one application logs // (GET /v2/applications/{application-id}/logs) func (si *ServerImplementation) LookupApplicationLogsByID(ctx echo.Context, applicationID uint64, params generated.LookupApplicationLogsByIDParams) error { @@ -934,6 +1072,29 @@ func (si *ServerImplementation) fetchApplications(ctx context.Context, params id return apps, round, nil } +// fetchApplications fetches all results +func (si *ServerImplementation) fetchApplicationBoxes(ctx context.Context, params idb.ApplicationBoxQuery) (appid generated.ApplicationId, boxes []generated.Box, round uint64, err error) { + boxes = make([]generated.Box, 0) + + err = callWithTimeout(ctx, si.log, si.timeout, func(ctx context.Context) error { + var results <-chan idb.ApplicationBoxRow + results, round = si.db.ApplicationBoxes(ctx, params) + + for result := range results { + if result.Error != nil { + return result.Error + } + if appid == 0 { + appid = generated.ApplicationId(result.App) + } + boxes = append(boxes, result.Box) + } + + return nil + }) + return +} + // fetchAppLocalStates fetches all generated.AppLocalState from a query func (si *ServerImplementation) fetchAppLocalStates(ctx context.Context, params idb.ApplicationQuery) ([]generated.ApplicationLocalState, uint64, error) { var round uint64 diff --git a/api/handlers_e2e_test.go b/api/handlers_e2e_test.go index bf416d301..a9572b918 100644 --- a/api/handlers_e2e_test.go +++ b/api/handlers_e2e_test.go @@ -24,6 +24,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/rpcs" "github.com/algorand/indexer/processor" @@ -54,9 +55,15 @@ var defaultOpts = ExtraOptions{ MaxApplicationsLimit: 1000, DefaultApplicationsLimit: 100, + MaxBoxesLimit: 10000, + DefaultBoxesLimit: 1000, + DisabledMapConfig: MakeDisabledMapConfig(), } +type boxTestComparator func(t *testing.T, db *postgres.IndexerDb, appBoxes map[basics.AppIndex]map[string]string, + deletedBoxes map[basics.AppIndex]map[string]bool, verifyTotals bool) + func testServerImplementation(db idb.IndexerDb) *ServerImplementation { return &ServerImplementation{db: db, timeout: 30 * time.Second, opts: defaultOpts} } @@ -1471,3 +1478,352 @@ func TestFetchBlockWithExpiredPartAccts(t *testing.T) { assert.Equal(t, test.AccountB.String(), expiredPartAccts[0]) assert.Equal(t, test.AccountC.String(), expiredPartAccts[1]) } + +// compareAppBoxesAgainstHandler is of type BoxTestComparator +func compareAppBoxesAgainstHandler(t *testing.T, db *postgres.IndexerDb, + appBoxes map[basics.AppIndex]map[string]string, deletedBoxes map[basics.AppIndex]map[string]bool, verifyTotals bool) { + + setupRequest := func(path, paramName, paramValue string) (echo.Context, *ServerImplementation, *httptest.ResponseRecorder) { + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + c.SetPath(path) + c.SetParamNames(paramName) + c.SetParamValues(paramValue) + api := testServerImplementation(db) + return c, api, rec + } + + remainingBoxes := map[basics.AppIndex]map[string]string{} + numRequests := 0 + sumOfBoxes := 0 + sumOfBoxBytes := 0 + + caseNum := 1 + var totalBoxes, totalBoxBytes int + for appIdx, boxes := range appBoxes { + totalBoxes = 0 + totalBoxBytes = 0 + + remainingBoxes[appIdx] = map[string]string{} + + // compare expected against handler response one box at a time + for key, expectedValue := range boxes { + msg := fmt.Sprintf("caseNum=%d, appIdx=%d, key=%#v", caseNum, appIdx, key) + expectedAppIdx, boxName, err := logic.SplitBoxKey(key) + require.NoError(t, err, msg) + require.Equal(t, appIdx, expectedAppIdx, msg) + numRequests++ + + boxDeleted := false + if deletedBoxes != nil { + if _, ok := deletedBoxes[appIdx][key]; ok { + boxDeleted = true + } + } + + c, api, rec := setupRequest("/v2/applications/:appidx/box/", "appidx", strconv.Itoa(int(appIdx))) + prefixedName := fmt.Sprintf("str:%s", boxName) + params := generated.LookupApplicationBoxByIDAndNameParams{Name: prefixedName} + err = api.LookupApplicationBoxByIDAndName(c, uint64(appIdx), params) + require.NoError(t, err, msg) + require.Equal(t, http.StatusOK, rec.Code, fmt.Sprintf("msg: %s. unexpected return code, body: %s", msg, rec.Body.String())) + + var resp generated.BoxResponse + data := rec.Body.Bytes() + err = json.Decode(data, &resp) + + if !boxDeleted { + require.NoError(t, err, msg, msg) + require.Equal(t, boxName, string(resp.Name), msg) + require.Equal(t, expectedValue, string(resp.Value), msg) + + remainingBoxes[appIdx][boxName] = expectedValue + + totalBoxes++ + totalBoxBytes += len(boxName) + len(expectedValue) + } else { + require.ErrorContains(t, err, "no rows in result set", msg) + } + } + + msg := fmt.Sprintf("caseNum=%d, appIdx=%d", caseNum, appIdx) + + expectedBoxes := remainingBoxes[appIdx] + + c, api, rec := setupRequest("/v2/applications/:appidx/boxes", "appidx", strconv.Itoa(int(appIdx))) + params := generated.SearchForApplicationBoxesParams{} + + err := api.SearchForApplicationBoxes(c, uint64(appIdx), params) + require.NoError(t, err, msg) + require.Equal(t, http.StatusOK, rec.Code, fmt.Sprintf("msg: %s. unexpected return code, body: %s", msg, rec.Body.String())) + + var resp generated.BoxesResponse + data := rec.Body.Bytes() + err = json.Decode(data, &resp) + require.NoError(t, err, msg) + + require.Equal(t, uint64(appIdx), uint64(resp.ApplicationId), msg) + + boxes := resp.Boxes + require.NotNil(t, boxes, msg) + require.Len(t, boxes, len(expectedBoxes), msg) + for _, box := range boxes { + require.Contains(t, expectedBoxes, string(box.Name), msg) + } + + if verifyTotals { + // compare expected totals against handler account_data JSON fields + msg := fmt.Sprintf("caseNum=%d, appIdx=%d", caseNum, appIdx) + + appAddr := appIdx.Address().String() + c, api, rec = setupRequest("/v2/accounts/:addr", "addr", appAddr) + fmt.Printf("appIdx=%d\nappAddr=%s\npath=/v2/accounts/%s\n", appIdx, appAddr, appAddr) + tru := true + params := generated.LookupAccountByIDParams{IncludeAll: &tru} + err := api.LookupAccountByID(c, appAddr, params) + require.NoError(t, err, msg) + require.Equal(t, http.StatusOK, rec.Code, fmt.Sprintf("msg: %s. unexpected return code, body: %s", msg, rec.Body.String())) + + var resp generated.AccountResponse + data := rec.Body.Bytes() + err = json.Decode(data, &resp) + + require.NoError(t, err, msg) + require.Equal(t, uint64(totalBoxes), resp.Account.TotalBoxes, msg) + require.Equal(t, uint64(totalBoxBytes), resp.Account.TotalBoxBytes, msg) + + // sanity check of the account summary query vs. the direct box search query results: + require.Equal(t, uint64(len(boxes)), resp.Account.TotalBoxes, msg) + } + + sumOfBoxes += totalBoxes + sumOfBoxBytes += totalBoxBytes + caseNum++ + } + + fmt.Printf("compareAppBoxesAgainstHandler succeeded with %d requests, %d boxes and %d boxBytes\n", numRequests, sumOfBoxes, sumOfBoxBytes) +} + +// test runner copy/pastad/tweaked in handlers_e2e_test.go and postgres_integration_test.go +func runBoxCreateMutateDelete(t *testing.T, comparator boxTestComparator) { + start := time.Now() + + db, shutdownFunc, proc, l := setupIdb(t, test.MakeGenesis()) + defer shutdownFunc() + defer l.Close() + + appid := basics.AppIndex(1) + + // ---- ROUND 1: create and fund the box app ---- // + currentRound := basics.Round(1) + + createTxn, err := test.MakeComplexCreateAppTxn(test.AccountA, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + + payNewAppTxn := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountA, appid.Address(), basics.Address{}, + basics.Address{}) + + block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &createTxn, &payNewAppTxn) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + opts := idb.ApplicationQuery{ApplicationID: uint64(appid)} + + rowsCh, round := db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + row, ok := <-rowsCh + require.True(t, ok) + require.NoError(t, row.Error) + require.NotNil(t, row.Application.CreatedAtRound) + require.Equal(t, uint64(currentRound), *row.Application.CreatedAtRound) + + // block header handoff: round 1 --> round 2 + blockHdr, err := l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 2: create 8 boxes for appid == 1 ---- // + currentRound = basics.Round(2) + + boxNames := []string{ + "a great box", + "another great box", + "not so great box", + "disappointing box", + "don't box me in this way", + "I will be assimilated", + "I'm destined for deletion", + "box #8", + } + + expectedAppBoxes := map[basics.AppIndex]map[string]string{} + + expectedAppBoxes[appid] = map[string]string{} + newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + boxTxns := make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range boxNames { + expectedAppBoxes[appid][logic.MakeBoxKey(appid, boxName)] = newBoxValue + + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + } + + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 2 --> round 3 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 3: populate the boxes appropriately ---- // + currentRound = basics.Round(3) + + appBoxesToSet := map[string]string{ + "a great box": "it's a wonderful box", + "another great box": "I'm wonderful too", + "not so great box": "bummer", + "disappointing box": "RUG PULL!!!!", + "don't box me in this way": "non box-conforming", + "I will be assimilated": "THE BORG", + "I'm destined for deletion": "I'm still alive!!!", + "box #8": "eight is beautiful", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + expectedAppBoxes[appid] = make(map[string]string) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 3 --> round 4 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 4: delete the unhappy boxes ---- // + currentRound = basics.Round(4) + + appBoxesToDelete := []string{ + "not so great box", + "disappointing box", + "I'm destined for deletion", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToDelete { + args := []string{"delete", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + delete(expectedAppBoxes[appid], key) + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + deletedBoxes := make(map[basics.AppIndex]map[string]bool) + deletedBoxes[appid] = make(map[string]bool) + for _, deletedBox := range appBoxesToDelete { + deletedBoxes[appid][deletedBox] = true + } + comparator(t, db, expectedAppBoxes, deletedBoxes, true) + + // block header handoff: round 4 --> round 5 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 5: create 3 new boxes, overwriting one of the former boxes ---- // + currentRound = basics.Round(5) + + appBoxesToCreate := []string{ + "fantabulous", + "disappointing box", // overwriting here + "AVM is the new EVM", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToCreate { + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = newBoxValue + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 5 --> round 6 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 6: populate the 3 new boxes ---- // + currentRound = basics.Round(6) + + appBoxesToSet = map[string]string{ + "fantabulous": "Italian food's the best!", // max char's + "disappointing box": "you made it!", + "AVM is the new EVM": "yes we can!", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + fmt.Printf("runBoxCreateMutateDelete total time: %s\n", time.Since(start)) +} + +// Test that box evolution is ingested as expected across rounds using API to compare +func TestBoxCreateMutateDeleteAgainstHandler(t *testing.T) { + runBoxCreateMutateDelete(t, compareAppBoxesAgainstHandler) +} diff --git a/api/handlers_test.go b/api/handlers_test.go index 2ca095f3c..5e9bb144f 100644 --- a/api/handlers_test.go +++ b/api/handlers_test.go @@ -19,8 +19,6 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/go-algorand-sdk/encoding/msgpack" - "github.com/algorand/go-algorand/crypto" - "github.com/algorand/go-algorand/crypto/merklesignature" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" @@ -752,47 +750,6 @@ func TestFetchAccountsRewindRoundTooLarge(t *testing.T) { assert.True(t, strings.HasPrefix(err.Error(), errRewindingAccount), err.Error()) } -// createTxn allows saving msgp-encoded canonical object to a file in order to add more test data -func createTxn(t *testing.T, target string) []byte { - defer assert.Fail(t, "this method should only be used for generating test inputs.") - addr1, err := basics.UnmarshalChecksumAddress("PT4K5LK4KYIQYYRAYPAZIEF47NVEQRDX3CPYWJVH25LKO2METIRBKRHRAE") - assert.Error(t, err) - var votePK crypto.OneTimeSignatureVerifier - votePK[0] = 1 - - var selectionPK crypto.VRFVerifier - selectionPK[0] = 1 - - var sprfkey merklesignature.Commitment - sprfkey[0] = 1 - - stxnad := transactions.SignedTxnWithAD{ - SignedTxn: transactions.SignedTxn{ - Txn: transactions.Transaction{ - Type: protocol.KeyRegistrationTx, - Header: transactions.Header{ - Sender: addr1, - }, - KeyregTxnFields: transactions.KeyregTxnFields{ - VotePK: votePK, - SelectionPK: selectionPK, - StateProofPK: sprfkey, - VoteFirst: basics.Round(0), - VoteLast: basics.Round(100), - VoteKeyDilution: 1000, - Nonparticipation: false, - }, - }, - }, - ApplyData: transactions.ApplyData{}, - } - - data := msgpack.Encode(stxnad) - err = ioutil.WriteFile(target, data, 0644) - assert.NoError(t, err) - return data -} - func TestLookupApplicationLogsByID(t *testing.T) { mockIndexer := &mocks.IndexerDb{} si := testServerImplementation(mockIndexer) diff --git a/api/indexer.oas2.json b/api/indexer.oas2.json index 14fe1272d..d3d42fa5b 100644 --- a/api/indexer.oas2.json +++ b/api/indexer.oas2.json @@ -485,6 +485,94 @@ } } }, + "/v2/applications/{application-id}/boxes": { + "get": { + "description": "Given an application ID, returns the box names of that application sorted lexicographically.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "operationId": "searchForApplicationBoxes", + "summary": "Get box names for a given application.", + "parameters": [ + { + "type": "integer", + "name": "application-id", + "in": "path", + "required": true + }, + { + "$ref": "#/parameters/limit" + }, + { + "$ref": "#/parameters/next" + } + ], + "responses": { + "200": { + "$ref": "#/responses/BoxesResponse" + }, + "400": { + "$ref": "#/responses/ErrorResponse" + }, + "404": { + "$ref": "#/responses/ErrorResponse" + }, + "500": { + "$ref": "#/responses/ErrorResponse" + } + } + } + }, + "/v2/applications/{application-id}/box": { + "get": { + "description": "Given an application ID and box name, returns base64 encoded box name and value. Box names must be in the goal app call arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, encode base 64 and use 'b64' prefix as in 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lookup" + ], + "operationId": "lookupApplicationBoxByIDAndName", + "schemes": [ + "http" + ], + "summary": "Get box information for a given application.", + "parameters": [ + { + "type": "integer", + "name": "application-id", + "in": "path", + "required": true + }, + { + "$ref": "#/parameters/box-name" + } + ], + "responses": { + "200": { + "$ref": "#/responses/BoxResponse" + }, + "400": { + "$ref": "#/responses/ErrorResponse" + }, + "404": { + "$ref": "#/responses/ErrorResponse" + }, + "500": { + "$ref": "#/responses/ErrorResponse" + } + } + } + }, "/v2/applications/{application-id}/logs": { "get": { "description": "Lookup application logs.", @@ -939,6 +1027,8 @@ "status", "total-apps-opted-in", "total-assets-opted-in", + "total-box-bytes", + "total-boxes", "total-created-apps", "total-created-assets" ], @@ -1031,6 +1121,14 @@ "description": "The count of all assets that have been opted in, equivalent to the count of AssetHolding objects held by this account.", "type": "integer" }, + "total-box-bytes": { + "description": "For app-accounts only. The total number of bytes allocated for the keys and values of boxes which belong to the associated application.", + "type": "integer" + }, + "total-boxes": { + "description": "For app-accounts only. The total number of boxes which belong to the associated application.", + "type": "integer" + }, "total-created-apps": { "description": "The count of all apps (AppParams objects) created by this account.", "type": "integer" @@ -1613,16 +1711,38 @@ } } }, - "ParticipationUpdates": { - "description": "Participation account data that needs to be checked/acted on by the network.", + "Box": { + "description": "Box name and its content.", + "required": [ + "name", + "value" + ], "type": "object", "properties": { - "expired-participation-accounts": { - "description": "\\[partupdrmv\\] a list of online accounts that needs to be converted to offline since their participation key expired.", - "type": "array", - "items": { - "type": "string" - } + "name": { + "description": "\\[name\\] box name, base64 encoded", + "format": "byte", + "type": "string" + }, + "value": { + "description": "\\[value\\] box value, base64 encoded.", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + } + } + }, + "BoxDescriptor": { + "description": "Box descriptor describes an app box without a value.", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Base64 encoded box name", + "format": "byte", + "type": "string" } } }, @@ -1711,6 +1831,19 @@ "delete" ] }, + "ParticipationUpdates": { + "description": "Participation account data that needs to be checked/acted on by the network.", + "type": "object", + "properties": { + "expired-participation-accounts": { + "description": "\\[partupdrmv\\] a list of online accounts that needs to be converted to offline since their participation key expired.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "StateDelta": { "description": "Application state delta.", "type": "array", @@ -2546,6 +2679,13 @@ "name": "before-time", "in": "query" }, + "box-name": { + "description": "A box name in goal-arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "type": "string", + "name": "name", + "in": "query", + "required": true + }, "currency-greater-than": { "type": "integer", "description": "Results should have an amount greater than this value. MicroAlgos are the default currency unless an asset-id is provided, in which case the asset will be used.", @@ -2901,6 +3041,38 @@ } } }, + "BoxesResponse": { + "description": "Box names of an application", + "schema": { + "type": "object", + "required": [ + "application-id", + "boxes" + ], + "properties": { + "application-id": { + "description": "\\[appidx\\] application index.", + "type": "integer" + }, + "boxes": { + "type": "array", + "items": { + "$ref": "#/definitions/BoxDescriptor" + } + }, + "next-token": { + "description": "Base64 encoded final box name result. Used for pagination, when making another request provide this token with the next parameter and prepend with \"b64:\" if keeping the provided encoding.", + "type": "string" + } + } + } + }, + "BoxResponse": { + "description": "Box information", + "schema": { + "$ref": "#/definitions/Box" + } + }, "ErrorResponse": { "description": "Response for errors", "schema":{ diff --git a/api/indexer.oas3.yml b/api/indexer.oas3.yml index cc72eff68..e339f2c77 100644 --- a/api/indexer.oas3.yml +++ b/api/indexer.oas3.yml @@ -81,6 +81,15 @@ }, "x-algorand-format": "RFC3339 String" }, + "box-name": { + "description": "A box name in goal-arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "in": "query", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + }, "currency-greater-than": { "description": "Results should have an amount greater than this value. MicroAlgos are the default currency unless an asset-id is provided, in which case the asset will be used.", "in": "query", @@ -542,6 +551,46 @@ }, "description": "(empty)" }, + "BoxResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Box" + } + } + }, + "description": "Box information" + }, + "BoxesResponse": { + "content": { + "application/json": { + "schema": { + "properties": { + "application-id": { + "description": "\\[appidx\\] application index.", + "type": "integer" + }, + "boxes": { + "items": { + "$ref": "#/components/schemas/BoxDescriptor" + }, + "type": "array" + }, + "next-token": { + "description": "Base64 encoded final box name result. Used for pagination, when making another request provide this token with the next parameter and prepend with \"b64:\" if keeping the provided encoding.", + "type": "string" + } + }, + "required": [ + "application-id", + "boxes" + ], + "type": "object" + } + } + }, + "description": "Box names of an application" + }, "ErrorResponse": { "content": { "application/json": { @@ -738,6 +787,14 @@ "description": "The count of all assets that have been opted in, equivalent to the count of AssetHolding objects held by this account.", "type": "integer" }, + "total-box-bytes": { + "description": "For app-accounts only. The total number of bytes allocated for the keys and values of boxes which belong to the associated application.", + "type": "integer" + }, + "total-boxes": { + "description": "For app-accounts only. The total number of boxes which belong to the associated application.", + "type": "integer" + }, "total-created-apps": { "description": "The count of all apps (AppParams objects) created by this account.", "type": "integer" @@ -757,6 +814,8 @@ "status", "total-apps-opted-in", "total-assets-opted-in", + "total-box-bytes", + "total-boxes", "total-created-apps", "total-created-assets" ], @@ -1293,6 +1352,43 @@ }, "type": "object" }, + "Box": { + "description": "Box name and its content.", + "properties": { + "name": { + "description": "\\[name\\] box name, base64 encoded", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + }, + "value": { + "description": "\\[value\\] box value, base64 encoded.", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "BoxDescriptor": { + "description": "Box descriptor describes an app box without a value.", + "properties": { + "name": { + "description": "Base64 encoded box name", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, "EvalDelta": { "description": "Represents a TEAL value delta.", "properties": { @@ -3612,6 +3708,247 @@ ] } }, + "/v2/applications/{application-id}/box": { + "get": { + "description": "Given an application ID and box name, returns base64 encoded box name and value. Box names must be in the goal app call arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, encode base 64 and use 'b64' prefix as in 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "operationId": "lookupApplicationBoxByIDAndName", + "parameters": [ + { + "in": "path", + "name": "application-id", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "A box name in goal-arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "in": "query", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Box" + } + } + }, + "description": "Box information" + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + }, + "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" + } + }, + "summary": "Get box information for a given application.", + "tags": [ + "lookup" + ] + } + }, + "/v2/applications/{application-id}/boxes": { + "get": { + "description": "Given an application ID, returns the box names of that application sorted lexicographically.", + "operationId": "searchForApplicationBoxes", + "parameters": [ + { + "in": "path", + "name": "application-id", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "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" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "application-id": { + "description": "\\[appidx\\] application index.", + "type": "integer" + }, + "boxes": { + "items": { + "$ref": "#/components/schemas/BoxDescriptor" + }, + "type": "array" + }, + "next-token": { + "description": "Base64 encoded final box name result. Used for pagination, when making another request provide this token with the next parameter and prepend with \"b64:\" if keeping the provided encoding.", + "type": "string" + } + }, + "required": [ + "application-id", + "boxes" + ], + "type": "object" + } + } + }, + "description": "Box names of an application" + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + }, + "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" + } + }, + "summary": "Get box names for a given application.", + "tags": [ + "search" + ] + } + }, "/v2/applications/{application-id}/logs": { "get": { "description": "Lookup application logs.", diff --git a/api/server.go b/api/server.go index c006d82ca..f5d9e920d 100644 --- a/api/server.go +++ b/api/server.go @@ -69,6 +69,10 @@ type ExtraOptions struct { // Applications MaxApplicationsLimit uint64 DefaultApplicationsLimit uint64 + + // Boxes + MaxBoxesLimit uint64 + DefaultBoxesLimit uint64 } func (e ExtraOptions) handlerTimeout() time.Duration { diff --git a/api/test_resources/boxes.json b/api/test_resources/boxes.json new file mode 100644 index 000000000..aff839bf2 --- /dev/null +++ b/api/test_resources/boxes.json @@ -0,0 +1,1526 @@ +{ + "file": "boxes.json", + "owner": "TestBoxes", + "lastModified": "2022-09-08 21:02:40.534512 -0500 CDT m=+3.770931351", + "frozen": true, + "cases": [ + { + "name": "What are all the accounts?", + "request": { + "path": "/v2/accounts", + "params": [], + "url": "http://localhost:8999/v2/accounts", + "route": "/v2/accounts" + }, + "response": { + "statusCode": 200, + "body": "{\"accounts\":[{\"address\":\"GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY\",\"amount\":999999499000,\"amount-without-pending-rewards\":999999499000,\"created-apps\":[{\"created-at-round\":1,\"deleted\":false,\"id\":1,\"params\":{\"approval-program\":\"CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=\",\"clear-state-program\":\"CIEB\",\"creator\":\"GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}}],\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"sig-type\":\"sig\",\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":1,\"total-created-assets\":0},{\"address\":\"LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU\",\"amount\":999999499000,\"amount-without-pending-rewards\":999999499000,\"created-apps\":[{\"created-at-round\":1,\"deleted\":false,\"id\":3,\"params\":{\"approval-program\":\"CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=\",\"clear-state-program\":\"CIEB\",\"creator\":\"N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}}],\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"sig-type\":\"sig\",\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":1,\"total-created-assets\":0},{\"address\":\"OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE\",\"amount\":999999499000,\"amount-without-pending-rewards\":999999499000,\"created-apps\":[{\"created-at-round\":1,\"deleted\":false,\"id\":5,\"params\":{\"approval-program\":\"CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=\",\"clear-state-program\":\"CIEB\",\"creator\":\"OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}}],\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"sig-type\":\"sig\",\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":1,\"total-created-assets\":0},{\"address\":\"WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":477,\"total-boxes\":9,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"ZROKLZW4GVOK5WQIF2GUR6LHFVEZBMV56BIQEQD4OTIZL2BPSYYUKFBSHM\",\"amount\":3000,\"amount-without-pending-rewards\":3000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":570,\"total-boxes\":13,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"4C3S3A5II6AYMEADSW7EVL7JAKVU2ASJMMJAGVUROIJHYMS6B24NCXVEWM\",\"amount\":100000,\"amount-without-pending-rewards\":100000,\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"status\":\"NotParticipating\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"6TB2ZQA2GEEDH6XTIOH5A7FUSGINXDPW5ONN6XBOBBGGUXVHRQTITAIIVI\",\"amount\":1000000000000,\"amount-without-pending-rewards\":1000000000000,\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0}],\"current-round\":8,\"next-token\":\"6TB2ZQA2GEEDH6XTIOH5A7FUSGINXDPW5ONN6XBOBBGGUXVHRQTITAIIVI\"}\n" + }, + "witness": { + "goType": "generated.AccountsResponse", + "accounts": { + "accounts": [ + { + "address": "GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY", + "amount": 999999499000, + "amount-without-pending-rewards": 999999499000, + "created-apps": [ + { + "created-at-round": 1, + "deleted": false, + "id": 1, + "params": { + "approval-program": "CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=", + "clear-state-program": "CIEB", + "creator": "GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + } + ], + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "sig-type": "sig", + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 1, + "total-created-assets": 0 + }, + { + "address": "LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU", + "amount": 999999499000, + "amount-without-pending-rewards": 999999499000, + "created-apps": [ + { + "created-at-round": 1, + "deleted": false, + "id": 3, + "params": { + "approval-program": "CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=", + "clear-state-program": "CIEB", + "creator": "N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + } + ], + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "sig-type": "sig", + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 1, + "total-created-assets": 0 + }, + { + "address": "OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE", + "amount": 999999499000, + "amount-without-pending-rewards": 999999499000, + "created-apps": [ + { + "created-at-round": 1, + "deleted": false, + "id": 5, + "params": { + "approval-program": "CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=", + "clear-state-program": "CIEB", + "creator": "OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + } + ], + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "sig-type": "sig", + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 1, + "total-created-assets": 0 + }, + { + "address": "WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 477, + "total-boxes": 9, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "ZROKLZW4GVOK5WQIF2GUR6LHFVEZBMV56BIQEQD4OTIZL2BPSYYUKFBSHM", + "amount": 3000, + "amount-without-pending-rewards": 3000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 570, + "total-boxes": 13, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "4C3S3A5II6AYMEADSW7EVL7JAKVU2ASJMMJAGVUROIJHYMS6B24NCXVEWM", + "amount": 100000, + "amount-without-pending-rewards": 100000, + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "status": "NotParticipating", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "6TB2ZQA2GEEDH6XTIOH5A7FUSGINXDPW5ONN6XBOBBGGUXVHRQTITAIIVI", + "amount": 1000000000000, + "amount-without-pending-rewards": 1000000000000, + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + } + ], + "current-round": 8, + "next-token": "6TB2ZQA2GEEDH6XTIOH5A7FUSGINXDPW5ONN6XBOBBGGUXVHRQTITAIIVI" + } + }, + "witnessError": null + }, + { + "name": "What are all the apps?", + "request": { + "path": "/v2/applications", + "params": [], + "url": "http://localhost:8999/v2/applications", + "route": "/v2/applications" + }, + "response": { + "statusCode": 200, + "body": "{\"applications\":[{\"created-at-round\":1,\"deleted\":false,\"id\":1,\"params\":{\"approval-program\":\"CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=\",\"clear-state-program\":\"CIEB\",\"creator\":\"GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}},{\"created-at-round\":1,\"deleted\":false,\"id\":3,\"params\":{\"approval-program\":\"CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=\",\"clear-state-program\":\"CIEB\",\"creator\":\"N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}},{\"created-at-round\":1,\"deleted\":false,\"id\":5,\"params\":{\"approval-program\":\"CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=\",\"clear-state-program\":\"CIEB\",\"creator\":\"OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}}],\"current-round\":8,\"next-token\":\"5\"}\n" + }, + "witness": { + "goType": "generated.ApplicationsResponse", + "apps": { + "applications": [ + { + "created-at-round": 1, + "deleted": false, + "id": 1, + "params": { + "approval-program": "CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=", + "clear-state-program": "CIEB", + "creator": "GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + }, + { + "created-at-round": 1, + "deleted": false, + "id": 3, + "params": { + "approval-program": "CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=", + "clear-state-program": "CIEB", + "creator": "N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + }, + { + "created-at-round": 1, + "deleted": false, + "id": 5, + "params": { + "approval-program": "CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=", + "clear-state-program": "CIEB", + "creator": "OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + } + ], + "current-round": 8, + "next-token": "5" + } + }, + "witnessError": null + }, + { + "name": "Lookup non-existing app 1337", + "request": { + "path": "/v2/applications/1337", + "params": [], + "url": "http://localhost:8999/v2/applications/1337", + "route": "/v2/applications/:application-id" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application found for application-id: 1337\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "Lookup app 3 (funded with no boxes)", + "request": { + "path": "/v2/applications/3", + "params": [], + "url": "http://localhost:8999/v2/applications/3", + "route": "/v2/applications/:application-id" + }, + "response": { + "statusCode": 200, + "body": "{\"application\":{\"created-at-round\":1,\"deleted\":false,\"id\":3,\"params\":{\"approval-program\":\"CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=\",\"clear-state-program\":\"CIEB\",\"creator\":\"N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}},\"current-round\":8}\n" + }, + "witness": { + "goType": "generated.ApplicationResponse", + "app": { + "application": { + "created-at-round": 1, + "deleted": false, + "id": 3, + "params": { + "approval-program": "CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=", + "clear-state-program": "CIEB", + "creator": "N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + }, + "current-round": 8 + } + }, + "witnessError": null + }, + { + "name": "Lookup app (funded with boxes)", + "request": { + "path": "/v2/applications/1", + "params": [], + "url": "http://localhost:8999/v2/applications/1", + "route": "/v2/applications/:application-id" + }, + "response": { + "statusCode": 200, + "body": "{\"application\":{\"created-at-round\":1,\"deleted\":false,\"id\":1,\"params\":{\"approval-program\":\"CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=\",\"clear-state-program\":\"CIEB\",\"creator\":\"GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}},\"current-round\":8}\n" + }, + "witness": { + "goType": "generated.ApplicationResponse", + "app": { + "application": { + "created-at-round": 1, + "deleted": false, + "id": 1, + "params": { + "approval-program": "CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=", + "clear-state-program": "CIEB", + "creator": "GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + }, + "current-round": 8 + } + }, + "witnessError": null + }, + { + "name": "Lookup app (funded with encoding test named boxes)", + "request": { + "path": "/v2/applications/5", + "params": [], + "url": "http://localhost:8999/v2/applications/5", + "route": "/v2/applications/:application-id" + }, + "response": { + "statusCode": 200, + "body": "{\"application\":{\"created-at-round\":1,\"deleted\":false,\"id\":5,\"params\":{\"approval-program\":\"CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=\",\"clear-state-program\":\"CIEB\",\"creator\":\"OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}},\"current-round\":8}\n" + }, + "witness": { + "goType": "generated.ApplicationResponse", + "app": { + "application": { + "created-at-round": 1, + "deleted": false, + "id": 5, + "params": { + "approval-program": "CCABADEYQQB1NhoAgAZjcmVhdGUSQQAYgSAxG4ECEkAABUg2GgIXNhoBTLlEQgBONhoAgAZkZWxldGUSQQAINhoBvERCADc2GgCAA3NldBJBAAs2GgEiNhoCu0IAIDYaAIAFY2hlY2sSQQARNhoBIjYaAhW6NhoCEkRCAAEAgQE=", + "clear-state-program": "CIEB", + "creator": "OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + }, + "current-round": 8 + } + }, + "witnessError": null + }, + { + "name": "Creator account - not an app account - no params", + "request": { + "path": "/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "params": [], + "url": "http://localhost:8999/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "route": "/v2/accounts/:account-id" + }, + "response": { + "statusCode": 200, + "body": "{\"account\":{\"address\":\"LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},\"current-round\":8}\n" + }, + "witness": { + "goType": "generated.AccountResponse", + "account": { + "account": { + "address": "LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + "current-round": 8 + } + }, + "witnessError": null + }, + { + "name": "App 3 (as account) totals no boxes - no params", + "request": { + "path": "/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "params": [], + "url": "http://localhost:8999/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "route": "/v2/accounts/:account-id" + }, + "response": { + "statusCode": 200, + "body": "{\"account\":{\"address\":\"LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},\"current-round\":8}\n" + }, + "witness": { + "goType": "generated.AccountResponse", + "account": { + "account": { + "address": "LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + "current-round": 8 + } + }, + "witnessError": null + }, + { + "name": "App 1 (as account) totals with boxes - no params", + "request": { + "path": "/v2/accounts/WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM", + "params": [], + "url": "http://localhost:8999/v2/accounts/WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM", + "route": "/v2/accounts/:account-id" + }, + "response": { + "statusCode": 200, + "body": "{\"account\":{\"address\":\"WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":8,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":477,\"total-boxes\":9,\"total-created-apps\":0,\"total-created-assets\":0},\"current-round\":8}\n" + }, + "witness": { + "goType": "generated.AccountResponse", + "account": { + "account": { + "address": "WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 8, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 477, + "total-boxes": 9, + "total-created-apps": 0, + "total-created-assets": 0 + }, + "current-round": 8 + } + }, + "witnessError": null + }, + { + "name": "Boxes of a app with id == math.MaxInt64", + "request": { + "path": "/v2/applications/9223372036854775807/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/9223372036854775807/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application found for application-id: round=8, appid=9223372036854775807\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "Boxes of a app with id == math.MaxInt64 + 1", + "request": { + "path": "/v2/applications/9223372036854775808/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/9223372036854775808/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"searching by round or application-id or asset-id or filter by value greater than 9223372036854775807 is not supported\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "Boxes of a non-existing app 1337", + "request": { + "path": "/v2/applications/1337/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/1337/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application found for application-id: round=8, appid=1337\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "Boxes of app 3 with no boxes: no params", + "request": { + "path": "/v2/applications/3/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/3/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":3,\"boxes\":[]}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 3, + "boxes": [] + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 5 with goal encoded boxes: no params", + "request": { + "path": "/v2/applications/5/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/5/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":5,\"boxes\":[{\"name\":\"AAAAAAAAACo=\"},{\"name\":\"AAAAAAAAAGQ=\"},{\"name\":\"AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgA==\"},{\"name\":\"WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=\"},{\"name\":\"YjMy\"},{\"name\":\"YjY0\"},{\"name\":\"YmFzZTMy\"},{\"name\":\"YmFzZTY0\"},{\"name\":\"Ynl0ZSBiYXNlMzI=\"},{\"name\":\"Ynl0ZSBiYXNlNjQ=\"},{\"name\":\"c3Ry\"},{\"name\":\"c3RyaW5n\"},{\"name\":\"1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=\"}],\"next-token\":\"b64:1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 5, + "boxes": [ + { + "name": "AAAAAAAAACo=" + }, + { + "name": "AAAAAAAAAGQ=" + }, + { + "name": "AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgA==" + }, + { + "name": "WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=" + }, + { + "name": "YjMy" + }, + { + "name": "YjY0" + }, + { + "name": "YmFzZTMy" + }, + { + "name": "YmFzZTY0" + }, + { + "name": "Ynl0ZSBiYXNlMzI=" + }, + { + "name": "Ynl0ZSBiYXNlNjQ=" + }, + { + "name": "c3Ry" + }, + { + "name": "c3RyaW5n" + }, + { + "name": "1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=" + } + ], + "next-token": "b64:1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: no params", + "request": { + "path": "/v2/applications/1/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/1/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"QVZNIGlzIHRoZSBuZXcgRVZN\"},{\"name\":\"SSB3aWxsIGJlIGFzc2ltaWxhdGVk\"},{\"name\":\"Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==\"},{\"name\":\"YSBncmVhdCBib3g=\"},{\"name\":\"YW5vdGhlciBncmVhdCBib3g=\"},{\"name\":\"Ym94ICM4\"},{\"name\":\"ZGlzYXBwb2ludGluZyBib3g=\"},{\"name\":\"ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5\"},{\"name\":\"ZmFudGFidWxvdXM=\"}],\"next-token\":\"b64:ZmFudGFidWxvdXM=\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "QVZNIGlzIHRoZSBuZXcgRVZN" + }, + { + "name": "SSB3aWxsIGJlIGFzc2ltaWxhdGVk" + }, + { + "name": "Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==" + }, + { + "name": "YSBncmVhdCBib3g=" + }, + { + "name": "YW5vdGhlciBncmVhdCBib3g=" + }, + { + "name": "Ym94ICM4" + }, + { + "name": "ZGlzYXBwb2ludGluZyBib3g=" + }, + { + "name": "ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5" + }, + { + "name": "ZmFudGFidWxvdXM=" + } + ], + "next-token": "b64:ZmFudGFidWxvdXM=" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - page 1", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"QVZNIGlzIHRoZSBuZXcgRVZN\"},{\"name\":\"SSB3aWxsIGJlIGFzc2ltaWxhdGVk\"},{\"name\":\"Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==\"}],\"next-token\":\"b64:Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "QVZNIGlzIHRoZSBuZXcgRVZN" + }, + { + "name": "SSB3aWxsIGJlIGFzc2ltaWxhdGVk" + }, + { + "name": "Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==" + } + ], + "next-token": "b64:Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - page 2 - b64", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "b64:Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=b64%3AUv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ%3D%3D", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"YSBncmVhdCBib3g=\"},{\"name\":\"YW5vdGhlciBncmVhdCBib3g=\"},{\"name\":\"Ym94ICM4\"}],\"next-token\":\"b64:Ym94ICM4\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "YSBncmVhdCBib3g=" + }, + { + "name": "YW5vdGhlciBncmVhdCBib3g=" + }, + { + "name": "Ym94ICM4" + } + ], + "next-token": "b64:Ym94ICM4" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - page 3 - b64", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "b64:Ym94ICM4" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=b64%3AYm94ICM4", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"ZGlzYXBwb2ludGluZyBib3g=\"},{\"name\":\"ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5\"},{\"name\":\"ZmFudGFidWxvdXM=\"}],\"next-token\":\"b64:ZmFudGFidWxvdXM=\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "ZGlzYXBwb2ludGluZyBib3g=" + }, + { + "name": "ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5" + }, + { + "name": "ZmFudGFidWxvdXM=" + } + ], + "next-token": "b64:ZmFudGFidWxvdXM=" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - MISSING b64 prefix", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "Ym94ICM4" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=Ym94ICM4", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"SearchForApplication received illegal next token (Ym94ICM4): all arguments and box names should be of the form 'encoding:value'\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - goal app arg encoding str", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "str:box #8" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=str%3Abox+%238", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"ZGlzYXBwb2ludGluZyBib3g=\"},{\"name\":\"ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5\"},{\"name\":\"ZmFudGFidWxvdXM=\"}],\"next-token\":\"b64:ZmFudGFidWxvdXM=\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "ZGlzYXBwb2ludGluZyBib3g=" + }, + { + "name": "ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5" + }, + { + "name": "ZmFudGFidWxvdXM=" + } + ], + "next-token": "b64:ZmFudGFidWxvdXM=" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - page 4 (empty) - b64", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "b64:ZmFudGFidWxvdXM=" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=b64%3AZmFudGFidWxvdXM%3D", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[]}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [] + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - ERROR because when next param provided -even empty string- it must be goal app arg encoded", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"SearchForApplication received illegal next token (): all arguments and box names should be of the form 'encoding:value'\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "Boxes (with made up name param) of a app with id == math.MaxInt64", + "request": { + "path": "/v2/applications/9223372036854775807/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/9223372036854775807/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application found for application-id: round=8, appid=9223372036854775807, boxName=string:non-existing\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "Box (with made up name param) of a app with id == math.MaxInt64 + 1", + "request": { + "path": "/v2/applications/9223372036854775808/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/9223372036854775808/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"searching by round or application-id or asset-id or filter by value greater than 9223372036854775807 is not supported\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "A box attempt for a non-existing app 1337", + "request": { + "path": "/v2/applications/1337/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/1337/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application found for application-id: round=8, appid=1337, boxName=string:non-existing\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "A box attempt for a non-existing app 1337 - without the required box name param", + "request": { + "path": "/v2/applications/1337/box", + "params": [], + "url": "http://localhost:8999/v2/applications/1337/box", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"Query argument name is required, but not found\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "A box attempt for a existing app 3 - without the required box name param", + "request": { + "path": "/v2/applications/3/box", + "params": [], + "url": "http://localhost:8999/v2/applications/3/box", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"Query argument name is required, but not found\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "App 3 box (non-existing)", + "request": { + "path": "/v2/applications/3/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/3/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application boxes found: round=8, appid=3, boxName=string:non-existing\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "App 1 box (non-existing)", + "request": { + "path": "/v2/applications/1/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/1/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application boxes found: round=8, appid=1, boxName=string:non-existing\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "App 1 box (a great box)", + "request": { + "path": "/v2/applications/1/box", + "params": [ + { + "name": "name", + "value": "string:a great box" + } + ], + "url": "http://localhost:8999/v2/applications/1/box?name=string%3Aa+great+box", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YSBncmVhdCBib3g=\",\"value\":\"aXQncyBhIHdvbmRlcmZ1bCBib3gAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YSBncmVhdCBib3g=", + "value": "aXQncyBhIHdvbmRlcmZ1bCBib3gAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (str:str) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "str:str" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=str%3Astr", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"c3Ry\",\"value\":\"c3RyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "c3Ry", + "value": "c3RyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (integer:100) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "integer:100" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=integer%3A100", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"AAAAAAAAAGQ=\",\"value\":\"AAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "AAAAAAAAAGQ=", + "value": "AAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (base32:MJQXGZJTGI======) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "base32:MJQXGZJTGI======" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=base32%3AMJQXGZJTGI%3D%3D%3D%3D%3D%3D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YmFzZTMy\",\"value\":\"YmFzZTMyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YmFzZTMy", + "value": "YmFzZTMyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (b64:YjY0) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "b64:YjY0" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=b64%3AYjY0", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YjY0\",\"value\":\"YjY0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YjY0", + "value": "YjY0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (base64:YmFzZTY0) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "base64:YmFzZTY0" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=base64%3AYmFzZTY0", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YmFzZTY0\",\"value\":\"YmFzZTY0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YmFzZTY0", + "value": "YmFzZTY0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (string:string) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "string:string" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=string%3Astring", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"c3RyaW5n\",\"value\":\"c3RyaW5nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "c3RyaW5n", + "value": "c3RyaW5nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (int:42) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "int:42" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=int%3A42", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"AAAAAAAAACo=\",\"value\":\"AAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "AAAAAAAAACo=", + "value": "AAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (abi:(uint64,string,bool[]):[399,\"pls pass\",[true,false]]) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "abi:(uint64,string,bool[]):[399,\"pls pass\",[true,false]]" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=abi%3A%28uint64%2Cstring%2Cbool%5B%5D%29%3A%5B399%2C%22pls+pass%22%2C%5Btrue%2Cfalse%5D%5D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgA==\",\"value\":\"AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgA==", + "value": "AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (addr:LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "addr:LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=addr%3ALMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=\",\"value\":\"WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=", + "value": "WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (address:2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "address:2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=address%3A2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=\",\"value\":\"1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=", + "value": "1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (b32:MIZTE===) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "b32:MIZTE===" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=b32%3AMIZTE%3D%3D%3D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YjMy\",\"value\":\"YjMyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YjMy", + "value": "YjMyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (byte base32:MJ4XIZJAMJQXGZJTGI======) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "byte base32:MJ4XIZJAMJQXGZJTGI======" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=byte+base32%3AMJ4XIZJAMJQXGZJTGI%3D%3D%3D%3D%3D%3D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"Ynl0ZSBiYXNlMzI=\",\"value\":\"Ynl0ZSBiYXNlMzIAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "Ynl0ZSBiYXNlMzI=", + "value": "Ynl0ZSBiYXNlMzIAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 encoding (byte base64:Ynl0ZSBiYXNlNjQ=) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "byte base64:Ynl0ZSBiYXNlNjQ=" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=byte+base64%3AYnl0ZSBiYXNlNjQ%3D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"Ynl0ZSBiYXNlNjQ=\",\"value\":\"Ynl0ZSBiYXNlNjQAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "Ynl0ZSBiYXNlNjQ=", + "value": "Ynl0ZSBiYXNlNjQAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "App 5 illegal encoding (just a plain string) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "just a plain string" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=just+a+plain+string", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"LookupApplicationBoxByIDAndName received illegal box name (just a plain string): all arguments and box names should be of the form 'encoding:value'\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "App 1337 non-existing with illegal encoding (just a plain string) - no params", + "request": { + "path": "/v2/applications/1337/box", + "params": [ + { + "name": "name", + "value": "just a plain string" + } + ], + "url": "http://localhost:8999/v2/applications/1337/box?name=just+a+plain+string", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"LookupApplicationBoxByIDAndName received illegal box name (just a plain string): all arguments and box names should be of the form 'encoding:value'\"}\n" + }, + "witness": null, + "witnessError": "400 error" + } + ] +} \ No newline at end of file diff --git a/cmd/algorand-indexer/daemon.go b/cmd/algorand-indexer/daemon.go index 3ef7254eb..4dc7ed1a6 100644 --- a/cmd/algorand-indexer/daemon.go +++ b/cmd/algorand-indexer/daemon.go @@ -79,6 +79,8 @@ type daemonConfig struct { defaultAccountsLimit uint32 maxAssetsLimit uint32 defaultAssetsLimit uint32 + maxBoxesLimit uint32 + defaultBoxesLimit uint32 maxBalancesLimit uint32 defaultBalancesLimit uint32 maxApplicationsLimit uint32 @@ -136,6 +138,8 @@ func DaemonCmd() *cobra.Command { cfg.flags.Uint32VarP(&cfg.defaultBalancesLimit, "default-balances-limit", "", 1000, "set the default Limit parameter for querying balances, if none is provided") cfg.flags.Uint32VarP(&cfg.maxApplicationsLimit, "max-applications-limit", "", 1000, "set the maximum allowed Limit parameter for querying applications") cfg.flags.Uint32VarP(&cfg.defaultApplicationsLimit, "default-applications-limit", "", 100, "set the default Limit parameter for querying applications, if none is provided") + cfg.flags.Uint32VarP(&cfg.maxBoxesLimit, "max-boxes-limit", "", 10000, "set the maximum allowed Limit parameter for searching an app's boxes") + cfg.flags.Uint32VarP(&cfg.defaultBoxesLimit, "default-boxes-limit", "", 1000, "set the default allowed Limit parameter for searching an app's boxes") cfg.flags.StringVarP(&cfg.indexerDataDir, "data-dir", "i", "", "path to indexer data dir, or $INDEXER_DATA") cfg.flags.BoolVar(&cfg.initLedger, "init-ledger", true, "initialize local ledger using sequential mode") @@ -456,6 +460,8 @@ func makeOptions(daemonConfig *daemonConfig) (options api.ExtraOptions) { options.DefaultBalancesLimit = uint64(daemonConfig.defaultBalancesLimit) options.MaxApplicationsLimit = uint64(daemonConfig.maxApplicationsLimit) options.DefaultApplicationsLimit = uint64(daemonConfig.defaultApplicationsLimit) + options.MaxBoxesLimit = uint64(daemonConfig.maxBoxesLimit) + options.DefaultBoxesLimit = uint64(daemonConfig.defaultBoxesLimit) if daemonConfig.enableAllParameters { options.DisabledMapConfig = api.MakeDisabledMapConfig() @@ -505,7 +511,7 @@ func blockHandler(proc processor.Processor, retryDelay time.Duration) func(conte case <-ctx.Done(): return err case <-time.After(retryDelay): - break + // NOOP } } } diff --git a/idb/dummy/dummy.go b/idb/dummy/dummy.go index 7e7c5b842..0fa142b32 100644 --- a/idb/dummy/dummy.go +++ b/idb/dummy/dummy.go @@ -83,6 +83,11 @@ func (db *dummyIndexerDb) AppLocalState(ctx context.Context, filter idb.Applicat return nil, 0 } +// ApplicationBoxes isn't currently implemented +func (db *dummyIndexerDb) ApplicationBoxes(ctx context.Context, filter idb.ApplicationBoxQuery) (<-chan idb.ApplicationBoxRow, uint64) { + panic("not implemented") +} + // Health is part of idb.IndexerDB func (db *dummyIndexerDb) Health(ctx context.Context) (state idb.Health, err error) { return idb.Health{}, nil diff --git a/idb/idb.go b/idb/idb.go index a45c4342a..8556845ea 100644 --- a/idb/idb.go +++ b/idb/idb.go @@ -182,6 +182,7 @@ type IndexerDb interface { AssetBalances(ctx context.Context, abq AssetBalanceQuery) (<-chan AssetBalanceRow, uint64) Applications(ctx context.Context, filter ApplicationQuery) (<-chan ApplicationRow, uint64) AppLocalState(ctx context.Context, filter ApplicationQuery) (<-chan AppLocalStateRow, uint64) + ApplicationBoxes(ctx context.Context, filter ApplicationBoxQuery) (<-chan ApplicationBoxRow, uint64) Health(ctx context.Context) (status Health, err error) } @@ -374,6 +375,23 @@ type AppLocalStateRow struct { Error error } +// ApplicationBoxQuery is a parameter object used to query application boxes. +type ApplicationBoxQuery struct { + ApplicationID uint64 + BoxName []byte + OmitValues bool + Ascending *bool + Limit uint64 + PrevFinalBox []byte +} + +// ApplicationBoxRow provides a response wrapping box information. +type ApplicationBoxRow struct { + App uint64 + Box models.Box + Error error +} + // IndexerDbOptions are the options common to all indexer backends. type IndexerDbOptions struct { ReadOnly bool diff --git a/idb/mocks/IndexerDb.go b/idb/mocks/IndexerDb.go index 55fe296c8..dbd7ca94d 100644 --- a/idb/mocks/IndexerDb.go +++ b/idb/mocks/IndexerDb.go @@ -60,6 +60,29 @@ func (_m *IndexerDb) AppLocalState(ctx context.Context, filter idb.ApplicationQu return r0, r1 } +// ApplicationBoxes provides a mock function with given fields: ctx, filter +func (_m *IndexerDb) ApplicationBoxes(ctx context.Context, filter idb.ApplicationBoxQuery) (<-chan idb.ApplicationBoxRow, uint64) { + ret := _m.Called(ctx, filter) + + var r0 <-chan idb.ApplicationBoxRow + if rf, ok := ret.Get(0).(func(context.Context, idb.ApplicationBoxQuery) <-chan idb.ApplicationBoxRow); ok { + r0 = rf(ctx, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan idb.ApplicationBoxRow) + } + } + + var r1 uint64 + if rf, ok := ret.Get(1).(func(context.Context, idb.ApplicationBoxQuery) uint64); ok { + r1 = rf(ctx, filter) + } else { + r1 = ret.Get(1).(uint64) + } + + return r0, r1 +} + // Applications provides a mock function with given fields: ctx, filter func (_m *IndexerDb) Applications(ctx context.Context, filter idb.ApplicationQuery) (<-chan idb.ApplicationRow, uint64) { ret := _m.Called(ctx, filter) diff --git a/idb/postgres/internal/writer/writer_test.go b/idb/postgres/internal/writer/writer_test.go index b6ca831f6..67a02c9cd 100644 --- a/idb/postgres/internal/writer/writer_test.go +++ b/idb/postgres/internal/writer/writer_test.go @@ -1565,6 +1565,102 @@ func TestWriterAddBlock0(t *testing.T) { assert.Equal(t, expected, accounts) } } +func getNameAndAccountPointer(t *testing.T, value ledgercore.ValueDelta, fullKey string, accts map[basics.Address]*ledgercore.AccountData) (basics.Address, string, *ledgercore.AccountData) { + require.NotNil(t, value, "cannot handle a nil value for box stats modification") + appIdx, name, err := logic.SplitBoxKey(fullKey) + account := appIdx.Address() + require.NoError(t, err) + acctData, ok := accts[account] + if !ok { + acctData = &ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{}, + } + accts[account] = acctData + } + return account, name, acctData +} + +func addBoxInfoToStats(t *testing.T, fullKey string, value ledgercore.ValueDelta, + accts map[basics.Address]*ledgercore.AccountData, boxTotals map[basics.Address]basics.AccountData) { + addr, name, acctData := getNameAndAccountPointer(t, value, fullKey, accts) + + acctData.TotalBoxes++ + acctData.TotalBoxBytes += uint64(len(name) + len(*value.Data)) + + boxTotals[addr] = basics.AccountData{ + TotalBoxes: acctData.TotalBoxes, + TotalBoxBytes: acctData.TotalBoxBytes, + } +} + +func subtractBoxInfoToStats(t *testing.T, fullKey string, value ledgercore.ValueDelta, + accts map[basics.Address]*ledgercore.AccountData, boxTotals map[basics.Address]basics.AccountData) { + addr, name, acctData := getNameAndAccountPointer(t, value, fullKey, accts) + + prevBoxBytes := uint64(len(name) + len(*value.Data)) + require.GreaterOrEqual(t, acctData.TotalBoxes, uint64(0)) + require.GreaterOrEqual(t, acctData.TotalBoxBytes, prevBoxBytes) + + acctData.TotalBoxes-- + acctData.TotalBoxBytes -= prevBoxBytes + + boxTotals[addr] = basics.AccountData{ + TotalBoxes: acctData.TotalBoxes, + TotalBoxBytes: acctData.TotalBoxBytes, + } +} + +// buildAccountDeltasFromKvsAndMods simulates keeping track of the evolution of the box statistics +func buildAccountDeltasFromKvsAndMods(t *testing.T, kvOriginals, kvMods map[string]ledgercore.ValueDelta) ( + ledgercore.StateDelta, map[string]ledgercore.ValueDelta, map[basics.Address]basics.AccountData) { + kvUpdated := map[string]ledgercore.ValueDelta{} + boxTotals := map[basics.Address]basics.AccountData{} + accts := map[basics.Address]*ledgercore.AccountData{} + /* + 1. fill the accts and kvUpdated using kvOriginals + 2. for each (fullKey, value) in kvMod: + * (A) if the key is not present in kvOriginals just add the info as in #1 + * (B) else (fullKey present): + * (i) if the value is nil + ==> remove the box info from the stats and kvUpdated with assertions + * (ii) else (value is NOT nil): + ==> reset kvUpdated and assert that the box hasn't changed shapes + */ + + /* 1. */ + for fullKey, value := range kvOriginals { + addBoxInfoToStats(t, fullKey, value, accts, boxTotals) + kvUpdated[fullKey] = value + } + + /* 2. */ + for fullKey, value := range kvMods { + prevValue, ok := kvOriginals[fullKey] + if !ok { + /* 2A. */ + addBoxInfoToStats(t, fullKey, value, accts, boxTotals) + kvUpdated[fullKey] = value + continue + } + /* 2B. */ + if value.Data == nil { + /* 2Bi. */ + subtractBoxInfoToStats(t, fullKey, prevValue, accts, boxTotals) + delete(kvUpdated, fullKey) + continue + } + /* 2Bii. */ + require.Equal(t, len(*prevValue.Data), len(*value.Data)) + require.Contains(t, kvUpdated, fullKey) + kvUpdated[fullKey] = value + } + + var delta ledgercore.StateDelta + for acct, acctData := range accts { + delta.Accts.Upsert(acct, *acctData) + } + return delta, kvUpdated, boxTotals +} // Simulate a scenario where app boxes are created, mutated and deleted in consecutive rounds. func TestWriterAppBoxTableInsertMutateDelete(t *testing.T) { @@ -1598,7 +1694,7 @@ func TestWriterAppBoxTableInsertMutateDelete(t *testing.T) { appID := basics.AppIndex(3) notPresent := "NOT PRESENT" - /*** FIRST ROUND - create 5 boxes ***/ + // ---- ROUND 1: create 5 boxes ---- // n1, v1 := "box1", "inserted" n2, v2 := "box2", "inserted" n3, v3 := "box3", "inserted" @@ -1618,7 +1714,7 @@ func TestWriterAppBoxTableInsertMutateDelete(t *testing.T) { delta.KvMods[k4] = ledgercore.ValueDelta{Data: &v4} delta.KvMods[k5] = ledgercore.ValueDelta{Data: &v5} - delta2, newKvMods, accts := test.BuildAccountDeltasFromKvsAndMods(t, map[string]ledgercore.ValueDelta{}, delta.KvMods) + delta2, newKvMods, accts := buildAccountDeltasFromKvsAndMods(t, map[string]ledgercore.ValueDelta{}, delta.KvMods) delta.Accts = delta2.Accts err := pgutil.TxWithRetry(db, serializable, addNewBlock, nil) @@ -1667,7 +1763,7 @@ func TestWriterAppBoxTableInsertMutateDelete(t *testing.T) { validateTotals() - /*** SECOND ROUND - mutate 2, delete 3, mutate 4, delete 5, create 6 ***/ + // ---- ROUND 2: mutate 2, delete 3, mutate 4, delete 5, create 6 ---- // v2 = "mutated" // v3 is "deleted" v4 = "mutated" @@ -1683,7 +1779,7 @@ func TestWriterAppBoxTableInsertMutateDelete(t *testing.T) { delta.KvMods[k5] = ledgercore.ValueDelta{Data: nil} delta.KvMods[k6] = ledgercore.ValueDelta{Data: &v6} - delta2, newKvMods, accts = test.BuildAccountDeltasFromKvsAndMods(t, newKvMods, delta.KvMods) + delta2, newKvMods, accts = buildAccountDeltasFromKvsAndMods(t, newKvMods, delta.KvMods) delta.Accts = delta2.Accts err = pgutil.TxWithRetry(db, serializable, addNewBlock, nil) @@ -1694,11 +1790,11 @@ func TestWriterAppBoxTableInsertMutateDelete(t *testing.T) { validateRow(n3, notPresent) validateRow(n4, v4) // new v4 validateRow(n5, notPresent) - validateRow(n6, v6) // inserted + validateRow(n6, v6) validateTotals() - /*** THIRD ROUND - delete 4, insert 5 ***/ + // ---- ROUND 3: delete 4, insert 5 ---- // // v4 is "deleted" v5 = "re-inserted" @@ -1707,7 +1803,7 @@ func TestWriterAppBoxTableInsertMutateDelete(t *testing.T) { delta.KvMods[k4] = ledgercore.ValueDelta{Data: nil} delta.KvMods[k5] = ledgercore.ValueDelta{Data: &v5} - delta2, newKvMods, accts = test.BuildAccountDeltasFromKvsAndMods(t, newKvMods, delta.KvMods) + delta2, newKvMods, accts = buildAccountDeltasFromKvsAndMods(t, newKvMods, delta.KvMods) delta.Accts = delta2.Accts err = pgutil.TxWithRetry(db, serializable, addNewBlock, nil) @@ -1724,7 +1820,7 @@ func TestWriterAppBoxTableInsertMutateDelete(t *testing.T) { /*** FOURTH ROUND - NOOP ***/ delta.KvMods = map[string]ledgercore.ValueDelta{} - delta2, _, accts = test.BuildAccountDeltasFromKvsAndMods(t, newKvMods, delta.KvMods) + delta2, _, accts = buildAccountDeltasFromKvsAndMods(t, newKvMods, delta.KvMods) delta.Accts = delta2.Accts err = pgutil.TxWithRetry(db, serializable, addNewBlock, nil) diff --git a/idb/postgres/postgres.go b/idb/postgres/postgres.go index 75934e9e0..07a6febb0 100644 --- a/idb/postgres/postgres.go +++ b/idb/postgres/postgres.go @@ -1031,58 +1031,61 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) { } { - var ad ledgercore.AccountData - ad, err = encoding.DecodeTrimmedLcAccountData(accountDataJSONStr) + var accountData ledgercore.AccountData + accountData, err = encoding.DecodeTrimmedLcAccountData(accountDataJSONStr) if err != nil { err = fmt.Errorf("account decode err (%s) %v", accountDataJSONStr, err) req.out <- idb.AccountRow{Error: err} break } - account.Status = statusStrings[ad.Status] - hasSel := !allZero(ad.SelectionID[:]) - hasVote := !allZero(ad.VoteID[:]) - hasStateProofkey := !allZero(ad.StateProofID[:]) + account.Status = statusStrings[accountData.Status] + hasSel := !allZero(accountData.SelectionID[:]) + hasVote := !allZero(accountData.VoteID[:]) + hasStateProofkey := !allZero(accountData.StateProofID[:]) if hasSel || hasVote || hasStateProofkey { part := new(models.AccountParticipation) if hasSel { - part.SelectionParticipationKey = ad.SelectionID[:] + part.SelectionParticipationKey = accountData.SelectionID[:] } if hasVote { - part.VoteParticipationKey = ad.VoteID[:] + part.VoteParticipationKey = accountData.VoteID[:] } if hasStateProofkey { - part.StateProofKey = byteSlicePtr(ad.StateProofID[:]) + part.StateProofKey = byteSlicePtr(accountData.StateProofID[:]) } - part.VoteFirstValid = uint64(ad.VoteFirstValid) - part.VoteLastValid = uint64(ad.VoteLastValid) - part.VoteKeyDilution = ad.VoteKeyDilution + part.VoteFirstValid = uint64(accountData.VoteFirstValid) + part.VoteLastValid = uint64(accountData.VoteLastValid) + part.VoteKeyDilution = accountData.VoteKeyDilution account.Participation = part } - if !ad.AuthAddr.IsZero() { + if !accountData.AuthAddr.IsZero() { var spendingkey basics.Address - copy(spendingkey[:], ad.AuthAddr[:]) + copy(spendingkey[:], accountData.AuthAddr[:]) account.AuthAddr = stringPtr(spendingkey.String()) } { totalSchema := models.ApplicationStateSchema{ - NumByteSlice: ad.TotalAppSchema.NumByteSlice, - NumUint: ad.TotalAppSchema.NumUint, + NumByteSlice: accountData.TotalAppSchema.NumByteSlice, + NumUint: accountData.TotalAppSchema.NumUint, } if totalSchema != (models.ApplicationStateSchema{}) { account.AppsTotalSchema = &totalSchema } } - if ad.TotalExtraAppPages != 0 { - account.AppsTotalExtraPages = uint64Ptr(uint64(ad.TotalExtraAppPages)) + if accountData.TotalExtraAppPages != 0 { + account.AppsTotalExtraPages = uint64Ptr(uint64(accountData.TotalExtraAppPages)) } - account.TotalAppsOptedIn = ad.TotalAppLocalStates - account.TotalCreatedApps = ad.TotalAppParams - account.TotalAssetsOptedIn = ad.TotalAssets - account.TotalCreatedAssets = ad.TotalAssetParams + account.TotalAppsOptedIn = accountData.TotalAppLocalStates + account.TotalCreatedApps = accountData.TotalAppParams + account.TotalAssetsOptedIn = accountData.TotalAssets + account.TotalCreatedAssets = accountData.TotalAssetParams + + account.TotalBoxes = accountData.TotalBoxes + account.TotalBoxBytes = accountData.TotalBoxBytes } if account.Status == "NotParticipating" { @@ -2288,6 +2291,125 @@ func (db *IndexerDb) yieldApplicationsThread(rows pgx.Rows, out chan idb.Applica } } +// ApplicationBoxes is part of interface idb.IndexerDB. The most complex query formed looks like: +// +// WITH apps AS (SELECT index AS app FROM app WHERE index = $1) +// SELECT a.app, ab.name, ab.value +// FROM apps a +// LEFT OUTER JOIN app_box ab ON ab.app = a.app AND name [= or >] $2 ORDER BY ab.name [ASC or DESC] LIMIT {queryOpts.Limit} +// +// where the binary operator in the last line is `=` for the box lookup and `>` for boxes search +// with query substitutions: +// $1 <-- queryOpts.ApplicationID +// $2 <-- queryOpts.BoxName +// $3 <-- queryOpts.PrevFinalBox +func (db *IndexerDb) ApplicationBoxes(ctx context.Context, queryOpts idb.ApplicationBoxQuery) (<-chan idb.ApplicationBoxRow, uint64) { + out := make(chan idb.ApplicationBoxRow, 1) + + columns := `a.app, ab.name` + if !queryOpts.OmitValues { + columns += `, ab.value` + } + query := fmt.Sprintf(`WITH apps AS (SELECT index AS app FROM app WHERE index = $1) +SELECT %s +FROM apps a +LEFT OUTER JOIN app_box ab ON ab.app = a.app`, columns) + + whereArgs := []interface{}{queryOpts.ApplicationID} + if queryOpts.BoxName != nil { + query += " AND name = $2" + whereArgs = append(whereArgs, queryOpts.BoxName) + } else if queryOpts.PrevFinalBox != nil { + query += " AND name > $2" + whereArgs = append(whereArgs, queryOpts.PrevFinalBox) + } + + orderKind := "ASC" + if queryOpts.Ascending != nil && !*queryOpts.Ascending { + orderKind = "DESC" + } + query += fmt.Sprintf(" ORDER BY ab.name %s", orderKind) + + if queryOpts.Limit != 0 { + query += fmt.Sprintf(" LIMIT %d", queryOpts.Limit) + } + + tx, err := db.db.BeginTx(ctx, readonlyRepeatableRead) + if err != nil { + out <- idb.ApplicationBoxRow{Error: err} + close(out) + return out, 0 + } + + round, err := db.getMaxRoundAccounted(ctx, tx) + if err != nil { + out <- idb.ApplicationBoxRow{Error: err} + close(out) + if rerr := tx.Rollback(ctx); rerr != nil { + db.log.Printf("rollback error: %s", rerr) + } + return out, round + } + + rows, err := tx.Query(ctx, query, whereArgs...) + if err != nil { + out <- idb.ApplicationBoxRow{Error: err} + close(out) + if rerr := tx.Rollback(ctx); rerr != nil { + db.log.Printf("rollback error: %s", rerr) + } + return out, round + } + + go func() { + db.yieldApplicationBoxThread(queryOpts.OmitValues, rows, 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) yieldApplicationBoxThread(omitValues bool, rows pgx.Rows, out chan idb.ApplicationBoxRow) { + defer rows.Close() + + gotRows := false + for rows.Next() { + gotRows = true + var app uint64 + var name []byte + var value []byte + var err error + + if omitValues { + err = rows.Scan(&app, &name) + } else { + err = rows.Scan(&app, &name, &value) + } + if err != nil { + out <- idb.ApplicationBoxRow{Error: err} + break + } + + box := models.Box{ + Name: name, + Value: value, // is nil when omitValues + } + + out <- idb.ApplicationBoxRow{App: app, Box: box} + } + if err := rows.Err(); err != nil { + out <- idb.ApplicationBoxRow{Error: err} + } else if !gotRows { + out <- idb.ApplicationBoxRow{Error: sql.ErrNoRows} + } +} + // AppLocalState is part of idb.IndexerDB func (db *IndexerDb) AppLocalState(ctx context.Context, filter idb.ApplicationQuery) (<-chan idb.AppLocalStateRow, uint64) { out := make(chan idb.AppLocalStateRow, 1) diff --git a/idb/postgres/postgres_boxes_test.go b/idb/postgres/postgres_boxes_test.go new file mode 100644 index 000000000..617b8ed93 --- /dev/null +++ b/idb/postgres/postgres_boxes_test.go @@ -0,0 +1,463 @@ +package postgres + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/jackc/pgx/v4" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/rpcs" + + "github.com/algorand/indexer/idb" + "github.com/algorand/indexer/idb/postgres/internal/encoding" + "github.com/algorand/indexer/idb/postgres/internal/writer" + "github.com/algorand/indexer/util/test" +) + +type boxTestComparator func(t *testing.T, db *IndexerDb, appBoxes map[basics.AppIndex]map[string]string, + deletedBoxes map[basics.AppIndex]map[string]bool, verifyTotals bool) + +// compareAppBoxesAgainstDB is of type testing.BoxTestComparator +func compareAppBoxesAgainstDB(t *testing.T, db *IndexerDb, + appBoxes map[basics.AppIndex]map[string]string, + deletedBoxes map[basics.AppIndex]map[string]bool, verifyTotals bool) { + + numQueries := 0 + sumOfBoxes := 0 + sumOfBoxBytes := 0 + + appBoxSQL := `SELECT app, name, value FROM app_box WHERE app = $1 AND name = $2` + acctDataSQL := `SELECT account_data FROM account WHERE addr = $1` + + caseNum := 1 + var totalBoxes, totalBoxBytes int + for appIdx, boxes := range appBoxes { + totalBoxes = 0 + totalBoxBytes = 0 + + // compare expected against db contents one box at a time + for key, expectedValue := range boxes { + msg := fmt.Sprintf("caseNum=%d, appIdx=%d, key=%#v", caseNum, appIdx, key) + expectedAppIdx, boxName, err := logic.SplitBoxKey(key) + require.NoError(t, err, msg) + require.Equal(t, appIdx, expectedAppIdx, msg) + + row := db.db.QueryRow(context.Background(), appBoxSQL, appIdx, []byte(boxName)) + numQueries++ + + boxDeleted := false + if deletedBoxes != nil { + if _, ok := deletedBoxes[appIdx][key]; ok { + boxDeleted = true + } + } + + var app basics.AppIndex + var name, value []byte + err = row.Scan(&app, &name, &value) + if !boxDeleted { + require.NoError(t, err, msg) + require.Equal(t, expectedAppIdx, app, msg) + require.Equal(t, boxName, string(name), msg) + require.Equal(t, expectedValue, string(value), msg) + + totalBoxes++ + totalBoxBytes += len(boxName) + len(expectedValue) + } else { + require.ErrorContains(t, err, "no rows in result set", msg) + } + } + if verifyTotals { + addr := appIdx.Address() + msg := fmt.Sprintf("caseNum=%d, appIdx=%d", caseNum, appIdx) + + row := db.db.QueryRow(context.Background(), acctDataSQL, addr[:]) + + var buf []byte + err := row.Scan(&buf) + require.NoError(t, err, msg) + + ret, err := encoding.DecodeTrimmedLcAccountData(buf) + require.NoError(t, err, msg) + require.Equal(t, uint64(totalBoxes), ret.TotalBoxes, msg) + require.Equal(t, uint64(totalBoxBytes), ret.TotalBoxBytes, msg) + } + + sumOfBoxes += totalBoxes + sumOfBoxBytes += totalBoxBytes + caseNum++ + } + + fmt.Printf("compareAppBoxesAgainstDB succeeded with %d queries, %d boxes and %d boxBytes\n", numQueries, sumOfBoxes, sumOfBoxBytes) +} + +// test runner copy/pastad/tweaked in handlers_e2e_test.go and postgres_integration_test.go +func runBoxCreateMutateDelete(t *testing.T, comparator boxTestComparator) { + start := time.Now() + + db, shutdownFunc, proc, l := setupIdb(t, test.MakeGenesis()) + defer shutdownFunc() + + defer l.Close() + + appid := basics.AppIndex(1) + + // ---- ROUND 1: create and fund the box app ---- // + currentRound := basics.Round(1) + + createTxn, err := test.MakeComplexCreateAppTxn(test.AccountA, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + + payNewAppTxn := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountA, appid.Address(), basics.Address{}, + basics.Address{}) + + block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &createTxn, &payNewAppTxn) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + opts := idb.ApplicationQuery{ApplicationID: uint64(appid)} + + rowsCh, round := db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + row, ok := <-rowsCh + require.True(t, ok) + require.NoError(t, row.Error) + require.NotNil(t, row.Application.CreatedAtRound) + require.Equal(t, uint64(currentRound), *row.Application.CreatedAtRound) + + // block header handoff: round 1 --> round 2 + blockHdr, err := l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 2: create 8 boxes for appid == 1 ---- // + currentRound = basics.Round(2) + + boxNames := []string{ + "a great box", + "another great box", + "not so great box", + "disappointing box", + "don't box me in this way", + "I will be assimilated", + "I'm destined for deletion", + "box #8", + } + + expectedAppBoxes := map[basics.AppIndex]map[string]string{} + + expectedAppBoxes[appid] = map[string]string{} + newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + boxTxns := make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range boxNames { + expectedAppBoxes[appid][logic.MakeBoxKey(appid, boxName)] = newBoxValue + + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + } + + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 2 --> round 3 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 3: populate the boxes appropriately ---- // + currentRound = basics.Round(3) + + appBoxesToSet := map[string]string{ + "a great box": "it's a wonderful box", + "another great box": "I'm wonderful too", + "not so great box": "bummer", + "disappointing box": "RUG PULL!!!!", + "don't box me in this way": "non box-conforming", + "I will be assimilated": "THE BORG", + "I'm destined for deletion": "I'm still alive!!!", + "box #8": "eight is beautiful", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + expectedAppBoxes[appid] = make(map[string]string) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 3 --> round 4 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 4: delete the unhappy boxes ---- // + currentRound = basics.Round(4) + + appBoxesToDelete := []string{ + "not so great box", + "disappointing box", + "I'm destined for deletion", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToDelete { + args := []string{"delete", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + delete(expectedAppBoxes[appid], key) + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + deletedBoxes := make(map[basics.AppIndex]map[string]bool) + deletedBoxes[appid] = make(map[string]bool) + for _, deletedBox := range appBoxesToDelete { + deletedBoxes[appid][deletedBox] = true + } + comparator(t, db, expectedAppBoxes, deletedBoxes, true) + + // block header handoff: round 4 --> round 5 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 5: create 3 new boxes, overwriting one of the former boxes ---- // + currentRound = basics.Round(5) + + appBoxesToCreate := []string{ + "fantabulous", + "disappointing box", // overwriting here + "AVM is the new EVM", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToCreate { + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = newBoxValue + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 5 --> round 6 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 6: populate the 3 new boxes ---- // + currentRound = basics.Round(6) + + appBoxesToSet = map[string]string{ + "fantabulous": "Italian food's the best!", // max char's + "disappointing box": "you made it!", + "AVM is the new EVM": "yes we can!", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + fmt.Printf("runBoxCreateMutateDelete total time: %s\n", time.Since(start)) +} + +// generateRandomBoxes generates a random slice of box keys and values for an app using future consensus params for guidance. +// NOTE: no attempt is made to adhere to the constraints BytesPerBoxReference etc. +func generateRandomBoxes(t *testing.T, appIdx basics.AppIndex, maxBoxes int) map[string]string { + future := config.Consensus[protocol.ConsensusFuture] + + numBoxes := rand.Intn(maxBoxes + 1) + boxes := make(map[string]string) + for i := 0; i < numBoxes; i++ { + nameLen := rand.Intn(future.MaxAppKeyLen + 1) + size := rand.Intn(int(future.MaxBoxSize) + 1) + + nameBytes := make([]byte, nameLen) + _, err := rand.Read(nameBytes) + require.NoError(t, err) + key := logic.MakeBoxKey(appIdx, string(nameBytes)) + + require.Positive(t, len(key)) + + valueBytes := make([]byte, size) + _, err = rand.Read(valueBytes) + require.NoError(t, err) + + boxes[key] = string(valueBytes) + } + return boxes +} + +func createRandomBoxesWithDelta(t *testing.T, numApps, maxBoxes int) (map[basics.AppIndex]map[string]string, ledgercore.StateDelta) { + appBoxes := make(map[basics.AppIndex]map[string]string) + + delta := ledgercore.StateDelta{ + KvMods: map[string]ledgercore.ValueDelta{}, + Accts: ledgercore.MakeAccountDeltas(numApps), + } + + for i := 0; i < numApps; i++ { + appIndex := basics.AppIndex(rand.Int63()) + boxes := generateRandomBoxes(t, appIndex, maxBoxes) + appBoxes[appIndex] = boxes + + for key, value := range boxes { + embeddedAppIdx, _, err := logic.SplitBoxKey(key) + require.NoError(t, err) + require.Equal(t, appIndex, embeddedAppIdx) + + val := string([]byte(value)[:]) + delta.KvMods[key] = ledgercore.ValueDelta{Data: &val} + } + + } + return appBoxes, delta +} + +func randomMutateSomeBoxesWithDelta(t *testing.T, appBoxes map[basics.AppIndex]map[string]string) ledgercore.StateDelta { + var delta ledgercore.StateDelta + delta.KvMods = make(map[string]ledgercore.ValueDelta) + + for _, boxes := range appBoxes { + for key, value := range boxes { + if rand.Intn(2) == 0 { + continue + } + valueBytes := make([]byte, len(value)) + _, err := rand.Read(valueBytes) + require.NoError(t, err) + boxes[key] = string(valueBytes) + + val := string([]byte(boxes[key])[:]) + delta.KvMods[key] = ledgercore.ValueDelta{Data: &val} + } + } + + return delta +} + +func deleteSomeBoxesWithDelta(t *testing.T, appBoxes map[basics.AppIndex]map[string]string) (map[basics.AppIndex]map[string]bool, ledgercore.StateDelta) { + deletedBoxes := make(map[basics.AppIndex]map[string]bool, len(appBoxes)) + + var delta ledgercore.StateDelta + delta.KvMods = make(map[string]ledgercore.ValueDelta) + + for appIndex, boxes := range appBoxes { + deletedBoxes[appIndex] = map[string]bool{} + for key := range boxes { + if rand.Intn(2) == 0 { + continue + } + deletedBoxes[appIndex][key] = true + delta.KvMods[key] = ledgercore.ValueDelta{Data: nil} + } + } + + return deletedBoxes, delta +} + +func addAppBoxesBlock(t *testing.T, db *IndexerDb, delta ledgercore.StateDelta) { + f := func(tx pgx.Tx) error { + w, err := writer.MakeWriter(tx) + require.NoError(t, err) + + err = w.AddBlock(&bookkeeping.Block{}, transactions.Payset{}, delta) + require.NoError(t, err) + + w.Close() + return nil + } + err := db.txWithRetry(serializable, f) + require.NoError(t, err) +} + +// Integration test for validating that box evolution is ingested as expected across rounds using database to compare +func TestBoxCreateMutateDeleteAgainstDB(t *testing.T) { + runBoxCreateMutateDelete(t, compareAppBoxesAgainstDB) +} + +// Write random apps with random box names and values, then read them from indexer DB and compare. +// NOTE: this does not populate TotalBoxes nor TotalBoxBytes deep under StateDeltas.Accts and therefore +// no query is taken to compare the summary box information in `account.account_data` +// Mutate some boxes and repeat the comparison. +// Delete some boxes and repeat the comparison. +func TestRandomWriteReadBoxes(t *testing.T) { + start := time.Now() + + db, shutdownFunc, _, ld := setupIdb(t, test.MakeGenesis()) + defer shutdownFunc() + defer ld.Close() + + appBoxes, delta := createRandomBoxesWithDelta(t, 10, 2500) + addAppBoxesBlock(t, db, delta) + compareAppBoxesAgainstDB(t, db, appBoxes, nil, false) + + delta = randomMutateSomeBoxesWithDelta(t, appBoxes) + addAppBoxesBlock(t, db, delta) + compareAppBoxesAgainstDB(t, db, appBoxes, nil, false) + + deletedBoxes, delta := deleteSomeBoxesWithDelta(t, appBoxes) + addAppBoxesBlock(t, db, delta) + compareAppBoxesAgainstDB(t, db, appBoxes, deletedBoxes, false) + + fmt.Printf("TestWriteReadBoxes total time: %s\n", time.Since(start)) +} diff --git a/idb/postgres/postgres_integration_test.go b/idb/postgres/postgres_integration_test.go index b95b7b210..e2abb3d78 100644 --- a/idb/postgres/postgres_integration_test.go +++ b/idb/postgres/postgres_integration_test.go @@ -19,7 +19,6 @@ import ( "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" - "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/rpcs" "github.com/algorand/go-codec/codec" @@ -2304,221 +2303,3 @@ func TestTransactionFilterAssetAmount(t *testing.T) { assert.Equal(t, txnF, *row.Txn) } - -// Test that box evolution is ingested as expected across rounds -func TestBoxCreateMutateDelete(t *testing.T) { - start := time.Now() - - db, shutdownFunc, proc, l := setupIdb(t, test.MakeGenesis()) - defer shutdownFunc() - - defer l.Close() - - appid := basics.AppIndex(1) - - /**** ROUND 1: create and fund the box app ****/ - currentRound := basics.Round(1) - - createTxn, err := test.MakeComplexCreateAppTxn(test.AccountA, test.BoxApprovalProgram, test.BoxClearProgram, 8) - require.NoError(t, err) - - payNewAppTxn := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountA, appid.Address(), basics.Address{}, - basics.Address{}) - - block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &createTxn, &payNewAppTxn) - require.NoError(t, err) - - err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) - require.NoError(t, err) - - opts := idb.ApplicationQuery{ApplicationID: uint64(appid)} - - rowsCh, round := db.Applications(context.Background(), opts) - require.Equal(t, uint64(currentRound), round) - - row, ok := <-rowsCh - require.True(t, ok) - require.NoError(t, row.Error) - require.NotNil(t, row.Application.CreatedAtRound) - require.Equal(t, uint64(currentRound), *row.Application.CreatedAtRound) - - // block header handoff: round 1 --> round 2 - blockHdr, err := l.BlockHdr(currentRound) - require.NoError(t, err) - - /**** ROUND 2: create 8 boxes for appid == 1 ****/ - currentRound = basics.Round(2) - - boxNames := []string{ - "a great box", - "another great box", - "not so great box", - "disappointing box", - "don't box me in this way", - "I will be assimilated", - "I'm destined for deletion", - "box #8", - } - - expectedAppBoxes := map[basics.AppIndex]map[string]string{} - - expectedAppBoxes[appid] = map[string]string{} - newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - boxTxns := make([]*transactions.SignedTxnWithAD, 0) - for _, boxName := range boxNames { - expectedAppBoxes[appid][logic.MakeBoxKey(appid, boxName)] = newBoxValue - - args := []string{"create", boxName} - boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) - boxTxns = append(boxTxns, &boxTxn) - } - - block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) - require.NoError(t, err) - - err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) - require.NoError(t, err) - _, round = db.Applications(context.Background(), opts) - require.Equal(t, uint64(currentRound), round) - - CompareAppBoxesAgainstDB(t, db, expectedAppBoxes) - - // block header handoff: round 2 --> round 3 - blockHdr, err = l.BlockHdr(currentRound) - require.NoError(t, err) - - /**** ROUND 3: populate the boxes appropriately ****/ - currentRound = basics.Round(3) - - appBoxesToSet := map[string]string{ - "a great box": "it's a wonderful box", - "another great box": "I'm wonderful too", - "not so great box": "bummer", - "disappointing box": "RUG PULL!!!!", - "don't box me in this way": "non box-conforming", - "I will be assimilated": "THE BORG", - "I'm destined for deletion": "I'm still alive!!!", - "box #8": "eight is beautiful", - } - - boxTxns = make([]*transactions.SignedTxnWithAD, 0) - expectedAppBoxes[appid] = make(map[string]string) - for boxName, valPrefix := range appBoxesToSet { - args := []string{"set", boxName, valPrefix} - boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) - boxTxns = append(boxTxns, &boxTxn) - - key := logic.MakeBoxKey(appid, boxName) - expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] - } - block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) - require.NoError(t, err) - - err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) - require.NoError(t, err) - _, round = db.Applications(context.Background(), opts) - require.Equal(t, uint64(currentRound), round) - - CompareAppBoxesAgainstDB(t, db, expectedAppBoxes) - - // block header handoff: round 3 --> round 4 - blockHdr, err = l.BlockHdr(currentRound) - require.NoError(t, err) - - /**** ROUND 4: delete the unhappy boxes ****/ - currentRound = basics.Round(4) - - appBoxesToDelete := []string{ - "not so great box", - "disappointing box", - "I'm destined for deletion", - } - - boxTxns = make([]*transactions.SignedTxnWithAD, 0) - for _, boxName := range appBoxesToDelete { - args := []string{"delete", boxName} - boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) - boxTxns = append(boxTxns, &boxTxn) - - key := logic.MakeBoxKey(appid, boxName) - delete(expectedAppBoxes[appid], key) - } - block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) - require.NoError(t, err) - - err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) - require.NoError(t, err) - _, round = db.Applications(context.Background(), opts) - require.Equal(t, uint64(currentRound), round) - - deletedBoxes := make(map[basics.AppIndex]map[string]bool) - deletedBoxes[appid] = make(map[string]bool) - for _, deletedBox := range appBoxesToDelete { - deletedBoxes[appid][deletedBox] = true - } - CompareAppBoxesAgainstDB(t, db, expectedAppBoxes, deletedBoxes) - - // block header handoff: round 4 --> round 5 - blockHdr, err = l.BlockHdr(currentRound) - require.NoError(t, err) - - /**** ROUND 5: create 3 new boxes, overwriting one of the former boxes ****/ - currentRound = basics.Round(5) - - appBoxesToCreate := []string{ - "fantabulous", - "disappointing box", // overwriting here - "AVM is the new EVM", - } - boxTxns = make([]*transactions.SignedTxnWithAD, 0) - for _, boxName := range appBoxesToCreate { - args := []string{"create", boxName} - boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) - boxTxns = append(boxTxns, &boxTxn) - - key := logic.MakeBoxKey(appid, boxName) - expectedAppBoxes[appid][key] = newBoxValue - } - block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) - require.NoError(t, err) - - err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) - require.NoError(t, err) - _, round = db.Applications(context.Background(), opts) - require.Equal(t, uint64(currentRound), round) - - CompareAppBoxesAgainstDB(t, db, expectedAppBoxes) - - // block header handoff: round 5 --> round 6 - blockHdr, err = l.BlockHdr(currentRound) - require.NoError(t, err) - - /**** ROUND 6: populate the 3 new boxes ****/ - currentRound = basics.Round(6) - - appBoxesToSet = map[string]string{ - "fantabulous": "Italian food's the best!", // max char's - "disappointing box": "you made it!", - "AVM is the new EVM": "yes we can!", - } - boxTxns = make([]*transactions.SignedTxnWithAD, 0) - for boxName, valPrefix := range appBoxesToSet { - args := []string{"set", boxName, valPrefix} - boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) - boxTxns = append(boxTxns, &boxTxn) - - key := logic.MakeBoxKey(appid, boxName) - expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] - } - block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) - require.NoError(t, err) - - err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) - require.NoError(t, err) - _, round = db.Applications(context.Background(), opts) - require.Equal(t, uint64(currentRound), round) - - CompareAppBoxesAgainstDB(t, db, expectedAppBoxes) - - fmt.Printf("TestBoxCreateMutateDelete total time: %s\n", time.Since(start)) -} diff --git a/idb/postgres/postgres_random_box_test.go b/idb/postgres/postgres_random_box_test.go deleted file mode 100644 index af9c8950a..000000000 --- a/idb/postgres/postgres_random_box_test.go +++ /dev/null @@ -1,225 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "math/rand" - "testing" - "time" - - "github.com/jackc/pgx/v4" - "github.com/stretchr/testify/require" - - "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/data/bookkeeping" - "github.com/algorand/go-algorand/data/transactions" - "github.com/algorand/go-algorand/data/transactions/logic" - "github.com/algorand/go-algorand/ledger/ledgercore" - "github.com/algorand/go-algorand/protocol" - - "github.com/algorand/indexer/idb/postgres/internal/writer" - "github.com/algorand/indexer/util/test" -) - -// generateBoxes generates a random slice of box keys and values for an app using future consensus params for guidance. -// NOTE: no attempt is made to adhere to the constraints BytesPerBoxReference etc. -func generateBoxes(t *testing.T, appIdx basics.AppIndex, maxBoxes int) map[string]string { - future := config.Consensus[protocol.ConsensusFuture] - - numBoxes := rand.Intn(maxBoxes + 1) - boxes := make(map[string]string) - for i := 0; i < numBoxes; i++ { - nameLen := rand.Intn(future.MaxAppKeyLen + 1) - size := rand.Intn(int(future.MaxBoxSize) + 1) - - nameBytes := make([]byte, nameLen) - _, err := rand.Read(nameBytes) - require.NoError(t, err) - key := logic.MakeBoxKey(appIdx, string(nameBytes)) - - require.Positive(t, len(key)) - - valueBytes := make([]byte, size) - _, err = rand.Read(valueBytes) - require.NoError(t, err) - - boxes[key] = string(valueBytes) - } - return boxes -} - -func createBoxesWithDelta(t *testing.T, numApps, maxBoxes int) (map[basics.AppIndex]map[string]string, ledgercore.StateDelta) { - appBoxes := make(map[basics.AppIndex]map[string]string) - - delta := ledgercore.StateDelta{ - KvMods: map[string]ledgercore.ValueDelta{}, - Accts: ledgercore.MakeAccountDeltas(numApps), - } - - for i := 0; i < numApps; i++ { - appIndex := basics.AppIndex(rand.Int63()) - boxes := generateBoxes(t, appIndex, maxBoxes) - appBoxes[appIndex] = boxes - - // totalBoxes := len(boxes) - totalBoxBytes := 0 - - for key, value := range boxes { - embeddedAppIdx, name, err := logic.SplitBoxKey(key) - require.NoError(t, err) - require.Equal(t, appIndex, embeddedAppIdx) - - val := string([]byte(value)[:]) - delta.KvMods[key] = ledgercore.ValueDelta{Data: &val} - - totalBoxBytes += len(name) + len(value) - } - - } - return appBoxes, delta -} - -func mutateSomeBoxesWithDelta(t *testing.T, appBoxes map[basics.AppIndex]map[string]string) ledgercore.StateDelta { - var delta ledgercore.StateDelta - delta.KvMods = make(map[string]ledgercore.ValueDelta) - - for _, boxes := range appBoxes { - for key, value := range boxes { - if rand.Intn(2) == 0 { - continue - } - valueBytes := make([]byte, len(value)) - _, err := rand.Read(valueBytes) - require.NoError(t, err) - boxes[key] = string(valueBytes) - - val := string([]byte(boxes[key])[:]) - delta.KvMods[key] = ledgercore.ValueDelta{Data: &val} - } - } - - return delta -} - -func deleteSomeBoxesWithDelta(t *testing.T, appBoxes map[basics.AppIndex]map[string]string) (map[basics.AppIndex]map[string]bool, ledgercore.StateDelta) { - deletedBoxes := make(map[basics.AppIndex]map[string]bool, len(appBoxes)) - - var delta ledgercore.StateDelta - delta.KvMods = make(map[string]ledgercore.ValueDelta) - - for appIndex, boxes := range appBoxes { - deletedBoxes[appIndex] = map[string]bool{} - for key := range boxes { - if rand.Intn(2) == 0 { - continue - } - deletedBoxes[appIndex][key] = true - delta.KvMods[key] = ledgercore.ValueDelta{Data: nil} - } - } - - return deletedBoxes, delta -} - -func addAppBoxesBlock(t *testing.T, db *IndexerDb, delta ledgercore.StateDelta) { - f := func(tx pgx.Tx) error { - w, err := writer.MakeWriter(tx) - require.NoError(t, err) - - err = w.AddBlock(&bookkeeping.Block{}, transactions.Payset{}, delta) - require.NoError(t, err) - - w.Close() - return nil - } - err := db.txWithRetry(serializable, f) - require.NoError(t, err) -} - -func CompareAppBoxesAgainstDB(t *testing.T, db *IndexerDb, - appBoxes map[basics.AppIndex]map[string]string, extras ...map[basics.AppIndex]map[string]bool) { - require.LessOrEqual(t, len(extras), 1) - var deletedBoxes map[basics.AppIndex]map[string]bool - if len(extras) == 1 { - deletedBoxes = extras[0] - } - - numQueries := 0 - sumOfBoxes := 0 - sumOfBoxBytes := 0 - - appBoxSQL := `SELECT app, name, value FROM app_box WHERE app = $1 AND name = $2` - - caseNum := 1 - var totalBoxes, totalBoxBytes int - for appIdx, boxes := range appBoxes { - totalBoxes = 0 - totalBoxBytes = 0 - - // compare expected against db contents one box at a time - for key, expectedValue := range boxes { - msg := fmt.Sprintf("caseNum=%d, appIdx=%d, key=%#v", caseNum, appIdx, key) - expectedAppIdx, boxName, err := logic.SplitBoxKey(key) - require.NoError(t, err, msg) - require.Equal(t, appIdx, expectedAppIdx, msg) - - row := db.db.QueryRow(context.Background(), appBoxSQL, appIdx, []byte(boxName)) - numQueries++ - - boxDeleted := false - if deletedBoxes != nil { - if _, ok := deletedBoxes[appIdx][key]; ok { - boxDeleted = true - } - } - - var app basics.AppIndex - var name, value []byte - err = row.Scan(&app, &name, &value) - if !boxDeleted { - require.NoError(t, err, msg) - require.Equal(t, expectedAppIdx, app, msg) - require.Equal(t, boxName, string(name), msg) - require.Equal(t, expectedValue, string(value), msg) - - totalBoxes++ - totalBoxBytes += len(boxName) + len(expectedValue) - } else { - require.ErrorContains(t, err, "no rows in result set", msg) - } - } - sumOfBoxes += totalBoxes - sumOfBoxBytes += totalBoxBytes - caseNum++ - } - - fmt.Printf("CompareAppBoxesAgainstDB succeeded with %d queries, %d boxes and %d boxBytes\n", numQueries, sumOfBoxes, sumOfBoxBytes) -} - -// Write random apps with random box names and values, then read them from indexer DB and compare. -// NOTE: this does not populate TotalBoxes nor TotalBoxBytes deep under StateDeltas.Accts and therefore -// no query is taken to compare the summary box information in `account.account_data` -// Mutate some boxes and repeat the comparison. -// Delete some boxes and repeat the comparison. -func TestWriteReadBoxes(t *testing.T) { - start := time.Now() - - db, shutdownFunc, _, ld := setupIdb(t, test.MakeGenesis()) - defer shutdownFunc() - defer ld.Close() - - appBoxes, delta := createBoxesWithDelta(t, 10, 2500) - addAppBoxesBlock(t, db, delta) - CompareAppBoxesAgainstDB(t, db, appBoxes) - - delta = mutateSomeBoxesWithDelta(t, appBoxes) - addAppBoxesBlock(t, db, delta) - CompareAppBoxesAgainstDB(t, db, appBoxes) - - deletedBoxes, delta := deleteSomeBoxesWithDelta(t, appBoxes) - addAppBoxesBlock(t, db, delta) - CompareAppBoxesAgainstDB(t, db, appBoxes, deletedBoxes) - - fmt.Printf("TestWriteReadBoxes total time: %s\n", time.Since(start)) -} diff --git a/misc/parity/reports/algod2indexer_full.yml b/misc/parity/reports/algod2indexer_full.yml index 2f6721ad4..c1458b71c 100644 --- a/misc/parity/reports/algod2indexer_full.yml +++ b/misc/parity/reports/algod2indexer_full.yml @@ -17,6 +17,16 @@ definitions: description: - INDEXER: '"Indicates what type of signature is used by this account, must be one of:\n* sig\n* msig\n* lsig\n* or null if unknown"' - ALGOD: '"Indicates what type of signature is used by this account, must be one of:\n* sig\n* msig\n* lsig"' + total-box-bytes: + - INDEXER: '{"description":"For app-accounts only. The total n...' + - ALGOD: null + total-boxes: + - INDEXER: '{"description":"For app-accounts only. The total n...' + - ALGOD: null required: + - - INDEXER: '"total-boxes"' + - ALGOD: null + - - INDEXER: '"total-box-bytes"' + - ALGOD: null - - INDEXER: null - ALGOD: '"min-balance"' diff --git a/misc/parity/test_indexer_v_algod.py b/misc/parity/test_indexer_v_algod.py index e5e6b87a1..28dc4f6d0 100644 --- a/misc/parity/test_indexer_v_algod.py +++ b/misc/parity/test_indexer_v_algod.py @@ -162,7 +162,7 @@ def test_parity(reports: List[str] = ASSERTIONS, save_new: bool = True): old_diff = yaml.safe_load(f) new_diff = generate_diff(algod_swgr, indexer_swgr, excludes, diff_type) - diff_of_diffs = deep_diff(old_diff, new_diff) + diff_of_diffs = deep_diff(old_diff, new_diff, arraysets=True) assert ( diff_of_diffs is None ), f"""UNEXPECTED CHANGE IN {ypath}. Differences are: diff --git a/processor/eval/ledger_for_evaluator_test.go b/processor/eval/ledger_for_evaluator_test.go index de3e9d132..628129608 100644 --- a/processor/eval/ledger_for_evaluator_test.go +++ b/processor/eval/ledger_for_evaluator_test.go @@ -2,6 +2,7 @@ package eval_test import ( "crypto/rand" + "fmt" "testing" test2 "github.com/sirupsen/logrus/hooks/test" @@ -543,9 +544,45 @@ func TestLedgerForEvaluatorAppCreatorMultiple(t *testing.T) { } } +// compareAppBoxesAgainstLedger uses LedgerForEvaluator to assert that provided app boxes can be retrieved as expected +func compareAppBoxesAgainstLedger(t *testing.T, ld indxLedger.LedgerForEvaluator, round basics.Round, + appBoxes map[basics.AppIndex]map[string]string, extras ...map[basics.AppIndex]map[string]bool) { + require.LessOrEqual(t, len(extras), 1) + var deletedBoxes map[basics.AppIndex]map[string]bool + if len(extras) == 1 { + deletedBoxes = extras[0] + } + + caseNum := 1 + for appIdx, boxes := range appBoxes { + for key, expectedValue := range boxes { + msg := fmt.Sprintf("caseNum=%d, appIdx=%d, key=%#v", caseNum, appIdx, key) + expectedAppIdx, _, err := logic.SplitBoxKey(key) + require.NoError(t, err, msg) + require.Equal(t, appIdx, expectedAppIdx, msg) + + boxDeleted := false + if deletedBoxes != nil { + if _, ok := deletedBoxes[appIdx][key]; ok { + boxDeleted = true + } + } + + value, err := ld.LookupKv(round, key) + require.NoError(t, err, msg) + if !boxDeleted { + require.Equal(t, expectedValue, *value, msg) + } else { + require.Nil(t, value, msg) + } + } + caseNum++ + } +} + // Test the functionality of `func (l LedgerForEvaluator) LookupKv()`. // This is done by handing off a pointer to Struct `processor/eval/ledger_for_evaluator.go::LedgerForEvaluator` -// in the `CompareAppBoxesAgainstLedger()` assertions function which then asserts using `LookupKv()` +// to `compareAppBoxesAgainstLedger()` which then asserts using `LookupKv()` func TestLedgerForEvaluatorLookupKv(t *testing.T) { logger, _ := test2.NewNullLogger() l := makeTestLedger(t) @@ -554,7 +591,7 @@ func TestLedgerForEvaluatorLookupKv(t *testing.T) { defer l.Close() defer ld.Close() - /**** ROUND 1: create and fund the box app ****/ + // ---- ROUND 1: create and fund the box app ---- // appid := basics.AppIndex(1) currentRound := basics.Round(1) @@ -581,7 +618,7 @@ func TestLedgerForEvaluatorLookupKv(t *testing.T) { blockHdr, err := l.BlockHdr(currentRound) require.NoError(t, err) - /**** ROUND 2: create 8 boxes of appid == 1 ****/ + // ---- ROUND 2: create 8 boxes of appid == 1 ---- // currentRound = basics.Round(2) boxNames := []string{ @@ -597,7 +634,7 @@ func TestLedgerForEvaluatorLookupKv(t *testing.T) { expectedAppBoxes := map[basics.AppIndex]map[string]string{} expectedAppBoxes[appid] = map[string]string{} - newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" boxTxns := make([]*transactions.SignedTxnWithAD, 0) for _, boxName := range boxNames { expectedAppBoxes[appid][logic.MakeBoxKey(appid, boxName)] = newBoxValue @@ -615,13 +652,13 @@ func TestLedgerForEvaluatorLookupKv(t *testing.T) { err = pr.Process(&rawBlock) require.NoError(t, err) - test.CompareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes) + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes) // block header handoff: round 2 --> round 3 blockHdr, err = l.BlockHdr(currentRound) require.NoError(t, err) - /**** ROUND 3: populate the boxes appropriately ****/ + // ---- ROUND 3: populate the boxes appropriately ---- // currentRound = basics.Round(3) appBoxesToSet := map[string]string{ @@ -652,13 +689,13 @@ func TestLedgerForEvaluatorLookupKv(t *testing.T) { err = pr.Process(&rawBlock) require.NoError(t, err) - test.CompareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes) + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes) // block header handoff: round 3 --> round 4 blockHdr, err = l.BlockHdr(currentRound) require.NoError(t, err) - /**** ROUND 4: delete the unhappy boxes ****/ + // ---- ROUND 4: delete the unhappy boxes ---- // currentRound = basics.Round(4) appBoxesToDelete := []string{ @@ -688,13 +725,13 @@ func TestLedgerForEvaluatorLookupKv(t *testing.T) { for _, deletedBox := range appBoxesToDelete { deletedBoxes[appid][deletedBox] = true } - test.CompareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes, deletedBoxes) + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes, deletedBoxes) // block header handoff: round 4 --> round 5 blockHdr, err = l.BlockHdr(currentRound) require.NoError(t, err) - /**** ROUND 5: create 3 new boxes, overwriting one of the former boxes ****/ + // ---- ROUND 5: create 3 new boxes, overwriting one of the former boxes ---- // currentRound = basics.Round(5) appBoxesToCreate := []string{ @@ -719,13 +756,13 @@ func TestLedgerForEvaluatorLookupKv(t *testing.T) { err = pr.Process(&rawBlock) require.NoError(t, err) - test.CompareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes) + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes) // block header handoff: round 5 --> round 6 blockHdr, err = l.BlockHdr(currentRound) require.NoError(t, err) - /**** ROUND 6: populate the 3 new boxes ****/ + // ---- ROUND 6: populate the 3 new boxes ---- // currentRound = basics.Round(6) appBoxesToSet = map[string]string{ @@ -749,7 +786,7 @@ func TestLedgerForEvaluatorLookupKv(t *testing.T) { err = pr.Process(&rawBlock) require.NoError(t, err) - test.CompareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes, deletedBoxes) + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes, deletedBoxes) } func TestLedgerForEvaluatorAccountTotals(t *testing.T) { diff --git a/util/test/box_testutil.go b/util/test/box_testutil.go deleted file mode 100644 index 474c130a4..000000000 --- a/util/test/box_testutil.go +++ /dev/null @@ -1,146 +0,0 @@ -package test - -import ( - "fmt" - "testing" - - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/data/transactions/logic" - "github.com/algorand/go-algorand/ledger/ledgercore" - - indxLedger "github.com/algorand/indexer/processor/eval" - "github.com/stretchr/testify/require" -) - -func getNameAndAccountPointer(t *testing.T, value ledgercore.ValueDelta, fullKey string, accts map[basics.Address]*ledgercore.AccountData) (basics.Address, string, *ledgercore.AccountData) { - require.NotNil(t, value, "cannot handle a nil value for box stats modification") - appIdx, name, err := logic.SplitBoxKey(fullKey) - account := appIdx.Address() - require.NoError(t, err) - acctData, ok := accts[account] - if !ok { - acctData = &ledgercore.AccountData{ - AccountBaseData: ledgercore.AccountBaseData{}, - } - accts[account] = acctData - } - return account, name, acctData -} - -func addBoxInfoToStats(t *testing.T, fullKey string, value ledgercore.ValueDelta, - accts map[basics.Address]*ledgercore.AccountData, boxTotals map[basics.Address]basics.AccountData) { - addr, name, acctData := getNameAndAccountPointer(t, value, fullKey, accts) - - acctData.TotalBoxes++ - acctData.TotalBoxBytes += uint64(len(name) + len(*value.Data)) - - boxTotals[addr] = basics.AccountData{ - TotalBoxes: acctData.TotalBoxes, - TotalBoxBytes: acctData.TotalBoxBytes, - } -} - -func subtractBoxInfoToStats(t *testing.T, fullKey string, value ledgercore.ValueDelta, - accts map[basics.Address]*ledgercore.AccountData, boxTotals map[basics.Address]basics.AccountData) { - addr, name, acctData := getNameAndAccountPointer(t, value, fullKey, accts) - - prevBoxBytes := uint64(len(name) + len(*value.Data)) - require.GreaterOrEqual(t, acctData.TotalBoxes, uint64(0)) - require.GreaterOrEqual(t, acctData.TotalBoxBytes, prevBoxBytes) - - acctData.TotalBoxes-- - acctData.TotalBoxBytes -= prevBoxBytes - - boxTotals[addr] = basics.AccountData{ - TotalBoxes: acctData.TotalBoxes, - TotalBoxBytes: acctData.TotalBoxBytes, - } -} - -// BuildAccountDeltasFromKvsAndMods simulates keeping track of the evolution of the box statistics -func BuildAccountDeltasFromKvsAndMods(t *testing.T, kvOriginals, kvMods map[string]ledgercore.ValueDelta) ( - ledgercore.StateDelta, map[string]ledgercore.ValueDelta, map[basics.Address]basics.AccountData) { - kvUpdated := map[string]ledgercore.ValueDelta{} - boxTotals := map[basics.Address]basics.AccountData{} - accts := map[basics.Address]*ledgercore.AccountData{} - /* - 1. fill the accts and kvUpdated using kvOriginals - 2. for each (fullKey, value) in kvMod: - * (A) if the key is not present in kvOriginals just add the info as in #1 - * (B) else (fullKey present): - * (i) if the value is nil - ==> remove the box info from the stats and kvUpdated with assertions - * (ii) else (value is NOT nil): - ==> reset kvUpdated and assert that the box hasn't changed shapes - */ - - /* 1. */ - for fullKey, value := range kvOriginals { - addBoxInfoToStats(t, fullKey, value, accts, boxTotals) - kvUpdated[fullKey] = value - } - - /* 2. */ - for fullKey, value := range kvMods { - prevValue, ok := kvOriginals[fullKey] - if !ok { - /* 2A. */ - addBoxInfoToStats(t, fullKey, value, accts, boxTotals) - kvUpdated[fullKey] = value - continue - } - /* 2B. */ - if value.Data == nil { - /* 2Bi. */ - subtractBoxInfoToStats(t, fullKey, prevValue, accts, boxTotals) - delete(kvUpdated, fullKey) - continue - } - /* 2Bii. */ - require.Equal(t, len(*prevValue.Data), len(*value.Data)) - require.Contains(t, kvUpdated, fullKey) - kvUpdated[fullKey] = value - } - - var delta ledgercore.StateDelta - for acct, acctData := range accts { - delta.Accts.Upsert(acct, *acctData) - } - return delta, kvUpdated, boxTotals -} - -// CompareAppBoxesAgainstLedger uses LedgerForEvaluator to assert that provided app boxes can be retrieved as expected -func CompareAppBoxesAgainstLedger(t *testing.T, ld indxLedger.LedgerForEvaluator, round basics.Round, - appBoxes map[basics.AppIndex]map[string]string, extras ...map[basics.AppIndex]map[string]bool) { - require.LessOrEqual(t, len(extras), 1) - var deletedBoxes map[basics.AppIndex]map[string]bool - if len(extras) == 1 { - deletedBoxes = extras[0] - } - - caseNum := 1 - for appIdx, boxes := range appBoxes { - for key, expectedValue := range boxes { - msg := fmt.Sprintf("caseNum=%d, appIdx=%d, key=%#v", caseNum, appIdx, key) - expectedAppIdx, _, err := logic.SplitBoxKey(key) - require.NoError(t, err, msg) - require.Equal(t, appIdx, expectedAppIdx, msg) - - boxDeleted := false - if deletedBoxes != nil { - if _, ok := deletedBoxes[appIdx][key]; ok { - boxDeleted = true - } - } - - value, err := ld.LookupKv(round, key) - require.NoError(t, err, msg) - if !boxDeleted { - require.Equal(t, expectedValue, *value, msg) - } else { - require.Nil(t, value, msg) - } - } - caseNum++ - } -} diff --git a/util/test/programs.go b/util/test/programs.go index 778c0c069..00a6e12ee 100644 --- a/util/test/programs.go +++ b/util/test/programs.go @@ -8,18 +8,18 @@ const BoxApprovalProgram string = `#pragma version 8 byte "create" // [arg[0], "create"] // create box named arg[1] == // [arg[0]=?="create"] bz del // "create" ? continue : goto del - int 24 // [24] - txn NumAppArgs // [24, NumAppArgs] - int 2 // [24, NumAppArgs, 2] - == // [24, NumAppArgs=?=2] + int 32 // [32] + txn NumAppArgs // [32, NumAppArgs] + int 2 // [32, NumAppArgs, 2] + == // [32, NumAppArgs=?=2] bnz default // WARNING: Assumes that when "create" provided, NumAppArgs >= 3 - pop // get rid of 24 // NumAppArgs != 2 + pop // get rid of 32 // NumAppArgs != 2 txn ApplicationArgs 2 // [arg[2]] // ERROR when NumAppArgs == 1 btoi // [btoi(arg[2])] -default: // [24] // NumAppArgs >= 3 - txn ApplicationArgs 1 // [24, arg[1]] - swap // [arg[1], 24] - box_create // [] // boxes: arg[1] -> [24]byte +default: // [32] // NumAppArgs >= 3 + txn ApplicationArgs 1 // [32, arg[1]] + swap // [arg[1], 32] + box_create // [] // boxes: arg[1] -> [32]byte assert b end del: // delete box arg[1]