From ce5e5f29f133cc470b10d45a7a4dca30d558ce33 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Wed, 9 Oct 2024 10:01:20 -0600 Subject: [PATCH] Use the bank module to keep track of scope value owners (#2140) * [2137]: Remove the MetadataKeeperI interface since it wasn't being used anywhere and anything trying to use the metadata keeper should define their own interface. * [2137]: Add the bank keeper to the metadata keeper. * [2137]: Add a Denom method to the MetadataAddress type. * [2137]: Add BurnCoins to the BankKeeper interface since that'll be needed when a scope is deleted. * [2137]: Remove all of the value-owner -> index stuff with a couple TODOs for re-implementation. * [2137]: Create a struct that associates an acc address with a metadata address. * [2137]: Fix the import name of bank types in expected keepers. * [2137]: Create GetScopeValueOwner and GetScopeValueOwners keeper methods. * [2137]: Expand the Denom test to include all the different types. * [2137]: Create a Coin method on the MetadataAddress type. * [2137]: Create ValidateAllAreScopes and Coins methods on the AccMDLinks type. * [2137]: Remove InputOutputCoins from the interface and add GetSupply. * [2137]: Add the module address to the metadata keeper so it can be reused when needed. * [2137]: Rip out some more stuff from the UpdateValueOwners and MigrateValueOwner endpoints to make it easier to change the stuff called in there. * [2137]: Create a method on the AccMDLinks type that will return a new slice with nils removed (if it's got nils in it to begin with). * [2137]: Refactor SetScopeValueOwners to use the bank module for the stuff. * [2137]: Remove the GetAccAddrs and GetMetadataAddrs methods from AccMDLinks since I couldn't actually use them. * [2137]: Put the MetadataAddress Prefix method back to the way it was because the new way was sometimes returning an error when it shouldn't. * [2137]: Update IterateScopes to populate the value owner field. Create GetScopeWithValueOwner that gets the scope with the value owner populated. Update the comment on GetScope to indicate that it no longer has the value owner field populated. * [2137]: In SetScope, clear out the value owner so that it'll always be empty now. * [2137]: Create a PopulateScopeValueOwner method and use that in IterateScopes and GetScopeWithValueOwner. * [2137]: Update the queries that get scopes to get them with the value owner populated. * [2137]: In ValidateWriteScope, get the scope with the value owner since it's used in there. * [2137]: Refactor ValidateAllAreScopes into ValidateForScopes and have it stop at the first error, check for nil entries, and also make sure the AccAddr isn't empty. Delete the WithNilsRemoved method. Add TODO to tweak MetadataAddress.String() to not panic when the address is invalid. * [2137]: Create ValidateIsScopeAddress since I was using that same code in a few places. * [2137]: Tweak an ordering of checks and include the offending string in an error in the UpdateValueOwnersCmd. * [2137]: Create SetScopeValueOwner as the only place to mint/burn stuff (and only works on a single scope), and refactor SetScopeValueOwners to require that all of the coins already exist. * [2137]: Remove GetSupply and SpendableCoins from the BankKeeper interface and in the MockBankKeeper, impelement BlockedAddr, MintCoins BurnCoins and SendCoins. * [2137]: In SetScopeValueOwner, do the DenomOwner lookup after checking the newValueOwner to prevent that lookup when the value owner is bad or blocked and include the burn error if there is one. * [2137]: Unit tests on SetScopeValueOwner. * [2137]: Add a comment about the error string (that's still todo, but will eventually be done) in MetadataAddress.String(). * [2137]: Create a function to convert a denom back into the metadata address. * [2137]: Re-implement the ValueOwnership query. * [2137]: Add the metedate module to the list of module accounts and give it mint and burn permission. * [2137]: Add a mint coins restriction to the metadata module that ensures it can only mint specific stuff. * [2137]: Update SetScope and RemoveScope to set/update/delete the value owner entry too. * [2137]: In the UpdateValueOwnersCmd, make it return the error when it's not a scope (insted of just making the error and moving on). * [2137]: Add cosmossdk.io/collections to the allowed imports list. * [2137]: Fix import ordering in expected keepers. * [2137]: Pay attention to the new new error return value from SetScope and SetScopeValueOwners. * [2137]: Update the pagination in ValueOwnership to use CollectionPaginate and include the denom prefix in the iterator prefix. * [2137]: Create the GetAccAddrs method on AccMDLinks (again) since I now need to use it. * [2137]: Update the marker context helpers to allow providing more than one admin to check permissions for. * [2137]: Update the ValidateUpdateValueOwners method and re-implement the UpdateValueOwners and MigrateValueOwner endpoints. * [2137]: Remove MsgUpdateValueOwnersRequest and MsgMigrateValueOwnerRequest from the message types that a MsgWriteScopeRequest authz grant works for. I.e. an authz grant for MsgWriteScopeRequest no longer confers usage of those value owner endpoints. * [2137]: Fix a couple uses of %w in non-error format strings. * [2137]: Remove a struct initilization that I originally used to have all the fields on-screen, but that served no functional purpose and should have been removed earlier. * [2137]: Redo unit tests on ValidateUpdateValueOwners and tweak it to just look for the required signers in the list instead of making a map for it. * [2137]: Add a todo to the bank keeper wrapper thingy to make it easier to mock the balances stuff. * [2137]: Create ValidateScopeValueOwnerSigned as a replacement to ValidateScopeValueOwnerUpdate and use it in ValidateWriteScope and ValidateDeleteScope. Mark ValidateScopeValueOwnerUpdate and some stuff only needed by it for removal. Have ValidateWriteScope look up the existig scope instead of haivng it be provided. Require there to be a value owner for new scopes (but might go back on that). * [2137]: Remove the DefaultParamspace variable since it's unused and fix the comment on NetAssetValueKeyPrefix. * [2137]: Create VerifyMetadataAddressHasType as a generic way to both validate a MetadataAddress and ensure it's a specific type. Switch ValidateIsScopeAddress to use this and create ValidateIsScopeSpecificationAddress that uses it. * [2137]: Update Scope.ValidateBasic to use the new ValidateIsScopeAddress and ValidateIsScopeSpecificationAddress. * [2137]: Split the scope writing portion of SetScope out into writeScopeToState so that it's easier to make unit tests that have a scope with a value owner in state. * [2137]: In MetadataAddress.String(), if it's not valid, have it output the %#v format instead of panicking. * [2137]: Add the transfer agents to the context in the endpoints that might try to update a value owner. * [2137]: Get rid of the AccMDLinks.Coins method since it wasn't used anymore. Clean up several comments and add some missing ones. * [2137]: Fix a few unit tests. TestSetScopeValueOwner started failling when I changed some error messages. TestValidateUpdateValueOwners needed some test cases on the returned addresses. TestWriteScopeSmartContractValueOwnerAuthz needed to have the scopes written so they could be looked up. * [2137]: Update RemoveScope to return an error instead of panicking. Fix the UpdateValueOwners to actually provide the links to SetScopeValueOwners. Fix a few spots that where trying to %q an AccAddress which makes it format as hex for some dumb reason. * [2137]: Delete the GetMarkerAndCheckAuthority, validateScopeValueOwnerChangeToProposed, validateScopeValueOwnerChangeFromExisting, and ValidateScopeValueOwnerUpdate methods since the latter isn't used anymore, and the others were only used by that flow. Create IsMarkerAddr and use it to not require markers to sign for value owner chagnes since the send restiction will check for permissions among the signers. * [2137]: Get rid of the GetBalancesCollection method on MDBankKeeper, and replace it with GetScopesForValueOwner. * [2137]: Unit tests on the DenomOwner MDBankKeeper method. * [2137]: Finish unit tests on the MDBankKeeper. * [2137]: Remove the mintCoinsRestriction since we're only minting in one place and its doing any needed checks. * [2137]: Replace the ValidateScopeValueOwnerSigned keeper method with ValidateScopeValueOwnersSigners. The new one has most of the content of ValidateUpdateValueOwners. Make all of WriteScope, DeleteScope, UpdateValueOwners, and MigrateValueOwner all use this same verification for value owner changes. Only provide the validatd signers as transfer agents. * [2137]: Take out the part where a value owner is required to write a scope and change the TODO on it to decide if we want to require it. * [2137]: Expand marker unit tests to account for the allowance of multiple transfer agents. * [2137]: Update marker flowcharts to reflect the possibility of multiple admins. * [2137]: In TestValidateDeleteScope, make it run all the tests when one panics. * [2137]: Unit tests on IsMarkerAddr. * [2137]: Create the IsMarkerAccount method on the marker keeper that does the check without having to get the account from auth. * [2137]: Get rid of the metadata module's version of IsMarkerAddr and switch to the marker module's version which is more efficient. * [2137]: Have ValidateScopeValueOwnersSigners return the transfer agents regardless of whether any of the existing accounts are marker accounts. It was either that or have it check the proposed too to see see if they might be needed. * [2137]: Unit tests on ValidateScopeValueOwnersSigners. * [2137]: Redo the tests on ValidateUpdateValueOwners since a bulk of that logic is now being tested on ValidateScopeValueOwnersSigners. * [2137]: Identify all of the keeper tests that fail and add TODOs to them. * [2137]: Identify all of the cli tests that fail and add TODOs to them. * [2137]: Move PartyDetails, UsedSignersMap, and the AuthzCache stuff out of the keeper and into types since they might be needed by people trying to use the metadata keeper. Also create the provutils package for some generic stuff that'd probably be handy in other places. * [2137]: Unit tests on Ternary and Pluralize and the rest of the signer utils that didn't have any yet. * [2137]: Update ValidateScopeValueOwnersSigners: If there's no existing owners, still process the signers and return them appropriately. * [2137]: In ValidateWriteScope, make sure there's a spec id. Update TestValidateWriteScope to fix all the stuff that has broken and add some new cases. * [2137]: Fix TestWriteScopeSmartContractValueOwnerAuthz. * [2137]: Overhaul and fix TestSetScopeValueOwners. * [2137]: Fix TestValidateDeleteScope and add a couple cases. * [2137]: Fix TestAddAndDeleteScopeDataAccess and TestAddAndDeleteScopeOwners. * [2137]: Redo the unit tests on the Scope query since they were failing and could use some added cases and stuff. * [2137]: Unit tests on the ScopesAll query. * [2137]: Remove the keeper's needsUpdate function since it's not needed anymore. Heh. * [2137]: Delete a couple TODO comments that are TODONE. * [2137]: Initial work on WriteScope tests. * [2137]: When reading a scope from state, clear out the ValueOwnerAddress field. * [2137]: Finish the unit tests on WriteScope. * [2137]: Create a Coins() method on the MetadataAddress struct that just wraps Coin(). * [2137]: Create the EventsBuilder in an attempt to make it easier to create the list of expected events for testing. * [2137]: Create a non-method way to write all the stuff in a dataSetup. * [2137]: Unit tests on the DeleteScope endpoint. Switch the WriteScope tests to use the new EventsBuilder. * [2137]: Fix TestScopesPagination. * [2137]: Re-enable TestScopeTxCommands. I must have fixed whatever had broken it earlier. * [2137]: Add a couple test cases to TestUpdateValueOwners, one of which fails because we're not requiring a signature for a scope that already has the desired value owner. * [2137]: Re-enable TestUpdateMigrateValueOwnersCmds and fix one of the failures. Need a code change to fix the others. * [2137]: Delete an empty line from the end of the TestDeleteOSLocator test runner. * [2137]: Create some helpers for writing stuff to the test logs. * [2137]: Create a GetMDAddrsForAccAddr method on the AccMDAddrs type. * [2137]: Add t.Helper() to the testlog stuff so that the log messages indicate the calling line instead of something inside logs.go. * [2137]: Get rid of the namedValue stuff in the metadata module and switch to using the testlog helpers. * [2137]: Switch GetMDAddrsForAccAddr to take in a string since that's what I've got where I want to use it. * [2137]: In ValidateUpdateValueOwners, return an error if any of the scopes already have the proposed value owner. * [2137]: Fix a test case in TestUpdateValueOwners that still had a TODO expected error. Get rid of the AssertErrorValue function defined in metadata/keeper/signers_test.go and use the one in our assertions package instead. * [2137]: Fix TestGetOwnershipCmd. It's the last of the broken unit tests. * [2137]: Fix the import section of keeper/signers_utils.go. * [2137]: In Scope.ValidateBasic, don't allow the spec id to be empty. * [2137]: Bump the metadata consensus version to 4 and create an empty migration to go from 3 to 4. * [2137]: Add TODO to update the spec docs. * [2137]: Create a Pair struct. Ugh. * [2137]: Write the metadata 3 to 4 migration. * [2137]: Remove the ValueOwnerScopeCacheKeyPrefix since it's now only need for the migration, which already has it. * [2137]: Delete the GetMaccPerms function since it's not used. * [2137]: Add the viridian upgrade. * [2137]: Create V3CreateScope to help facilitate testing, and switch the existing migration tests to use that. * [2137]: Fix import ordering in app/upgrades.go. * [2137]: Change the name of the panic-checking func in TestValidateWriteScope since it's the same as another function now. * [2137]: Write an upgrades test that creates 300,005 scopes and migrates them and checks things after. * [2137]: Bypass the marker send restriction in the metadata migration. * [2137]: Update the comment on the value_owner_address field in the proto. * [2137]: Update the line numbers in the spec docs links to scope.proto and also update the copy of the Scope message in there due to the new value owner comment. * [2137]: Update the spec docs to reflect the new value owner storage. Also fix a few typos in there. * [2137]: Add some changelog entries. * [2137]: Add a changelog entry for the viridian upgrade. * [2137]: Create a GetNetAssetValue method in the metadata module so that the exchange module can use it. * [2137]: In the exchange module, record scope navs when applicable. * [2137]: Use the correct formatting verb when logging the error about a metadata denom string not being valid. * [2137]: Update unit tests on the exchange keeper's GetNav method. * [2137]: Add some exchange test stuff involving scopes. * [2137]: Update the comments related to #2160. * [2137]: Tweak the migration to remove a panic that would happen if any scope somehow has an invalid value owner. Add a test case to TestSetScopeValueOwner to make sure nothing happens if the coin doesn't exist yet and an empty new value owner is provided. * [2137]: Unit tests on AtLeastOneAddrHasAccess and ValidateAtLeastOneAddrHasAccess. * [2137]: Update WithTransferAgents to set the value to a nil list of addresses instead of just a nil address. * [2137]: Fix a couple typos in the testlog stuff. * [2137]: Change the first arg of AddBurnCoinsStrs to moduleAddr for consistency. * [2137]: Fix a typo in a slices test case. * [2137]: Provide the scope id to emitMetadataNAVEvents. * [2137]: Remove SpendableCoin from the BankKeeper and fix a couple MockBankKeeper method comments. * [2137]: Remove the panic from RemoveScope and instead return an error. * [2137]: In TestUsedSignersMap, move the isUsed checks into the test runner. * [2137]: Swap out some TODOs involving metadata nav volume in the keeper scope tests. * [2137]: In the metadata keeper's GetNetAssetValue method, default the Volume to 1 if it's read in as zero from state (which means the field didn't exist when it was originally written. * [2137]: Update the stuff in the exchange module that needs to use the new Volume field in the metadata NAVs. * [2137]: Fix the WriteScope tests that should not be expecting a NAV in a bunch of them anymore. * [2137]: Fix the exchange tests that failed with the addition of Volume to the metadata NAV. * [2137]: Remove a couple unused proto imports. * [2137]: Regenerate stuff from the protos. --- .../2137-bank-scope-value-owners.md | 10 + .../features/2137-bank-scope-value-owners.md | 1 + .../2137-bank-scope-value-owners.md | 1 + .golangci.yml | 1 + app/app.go | 13 +- app/app_test.go | 5 - app/upgrades.go | 26 + app/upgrades_test.go | 234 ++ client/docs/swagger-ui/swagger.yaml | 566 +++- docs/proto-docs.md | 2 +- internal/provutils/pair.go | 47 + internal/provutils/pair_test.go | 163 + internal/provutils/slices.go | 29 + internal/provutils/slices_test.go | 470 +++ internal/provutils/ternary.go | 29 + internal/provutils/ternary_test.go | 124 + proto/cosmos/quarantine/v1beta1/genesis.proto | 1 - proto/cosmos/quarantine/v1beta1/tx.proto | 1 - proto/provenance/metadata/v1/scope.proto | 16 +- testutil/events_builder.go | 187 ++ testutil/testlog/logs.go | 138 + x/exchange/expected_keepers.go | 6 + x/exchange/keeper/commitments.go | 10 +- x/exchange/keeper/commitments_test.go | 34 + x/exchange/keeper/export_test.go | 6 + x/exchange/keeper/fulfillment.go | 107 +- x/exchange/keeper/fulfillment_test.go | 151 +- x/exchange/keeper/keeper.go | 13 +- x/exchange/keeper/market.go | 2 +- x/exchange/keeper/mocks_test.go | 233 +- x/exchange/keeper/msg_server_test.go | 24 +- x/exchange/keeper/suite_test.go | 25 +- x/marker/keeper/keeper.go | 9 + x/marker/keeper/keeper_test.go | 75 +- x/marker/keeper/send_restrictions.go | 51 +- x/marker/keeper/send_restrictions_test.go | 201 +- x/marker/spec/12_transfers.md | 33 +- x/marker/types/marker.go | 25 + x/marker/types/marker_test.go | 116 +- x/marker/types/send_restrictions.go | 21 +- x/marker/types/send_restrictions_test.go | 137 +- x/metadata/client/cli/cli_page_test.go | 10 +- x/metadata/client/cli/cli_test.go | 5 +- x/metadata/client/cli/tx.go | 6 +- x/metadata/keeper/account_data_test.go | 8 +- x/metadata/keeper/bank.go | 87 + x/metadata/keeper/bank_test.go | 561 ++++ x/metadata/keeper/expected_keepers.go | 15 + x/metadata/keeper/export_test.go | 159 +- x/metadata/keeper/genesis.go | 4 +- x/metadata/keeper/keeper.go | 102 +- x/metadata/keeper/keeper_test.go | 18 +- x/metadata/keeper/migrations_v4.go | 169 + x/metadata/keeper/migrations_v4_test.go | 641 ++++ x/metadata/keeper/mocks_test.go | 328 +- x/metadata/keeper/msg_server.go | 87 +- x/metadata/keeper/msg_server_test.go | 1202 ++++++- x/metadata/keeper/query_server.go | 35 +- x/metadata/keeper/query_server_test.go | 1030 +++++- x/metadata/keeper/record.go | 9 +- x/metadata/keeper/record_test.go | 3 +- x/metadata/keeper/scope.go | 476 ++- x/metadata/keeper/scope_test.go | 2200 ++++++++---- x/metadata/keeper/session.go | 4 +- x/metadata/keeper/session_test.go | 3 +- x/metadata/keeper/signers.go | 278 +- x/metadata/keeper/signers_test.go | 1848 ++++------ x/metadata/keeper/signers_utils.go | 367 +- x/metadata/keeper/signers_utils_test.go | 2961 +---------------- x/metadata/keeper/specification.go | 5 +- x/metadata/keeper/specification_test.go | 2 +- x/metadata/module.go | 16 +- x/metadata/spec/01_concepts.md | 3 +- x/metadata/spec/02_state.md | 36 +- x/metadata/spec/03_messages.md | 20 +- x/metadata/types/address.go | 229 +- x/metadata/types/address_test.go | 1506 ++++++++- x/metadata/types/keys.go | 17 +- x/metadata/types/scope.go | 27 +- x/metadata/types/scope.pb.go | 16 +- x/metadata/types/scope_test.go | 251 +- x/metadata/types/signer_utils.go | 429 +++ x/metadata/types/signer_utils_test.go | 2470 ++++++++++++++ x/quarantine/genesis.pb.go | 34 +- x/quarantine/tx.pb.go | 84 +- 85 files changed, 14731 insertions(+), 6373 deletions(-) create mode 100644 .changelog/unreleased/api-breaking/2137-bank-scope-value-owners.md create mode 100644 .changelog/unreleased/features/2137-bank-scope-value-owners.md create mode 100644 .changelog/unreleased/improvements/2137-bank-scope-value-owners.md create mode 100644 internal/provutils/pair.go create mode 100644 internal/provutils/pair_test.go create mode 100644 internal/provutils/slices.go create mode 100644 internal/provutils/slices_test.go create mode 100644 internal/provutils/ternary.go create mode 100644 internal/provutils/ternary_test.go create mode 100644 testutil/events_builder.go create mode 100644 testutil/testlog/logs.go create mode 100644 x/metadata/keeper/bank.go create mode 100644 x/metadata/keeper/bank_test.go create mode 100644 x/metadata/keeper/migrations_v4.go create mode 100644 x/metadata/keeper/migrations_v4_test.go create mode 100644 x/metadata/types/signer_utils.go create mode 100644 x/metadata/types/signer_utils_test.go diff --git a/.changelog/unreleased/api-breaking/2137-bank-scope-value-owners.md b/.changelog/unreleased/api-breaking/2137-bank-scope-value-owners.md new file mode 100644 index 0000000000..bdde823181 --- /dev/null +++ b/.changelog/unreleased/api-breaking/2137-bank-scope-value-owners.md @@ -0,0 +1,10 @@ +* The `Ownership` query in the `x/metadata` module now only returns scopes that have the provided address in the `owners` list [#2137](https://github.com/provenance-io/provenance/issues/2137). + Previously, if an address was the value owner of a scope, but not in the `owners` list, the scope would be returned + by the `Ownership` query when given that address. That is no longer the case. + The `ValueOwnership` query can be to identify scopes with a specific value owner (like before). + If a scope has a value owner that is also in its `owners` list, it will still be returned by both queries. +* The `WriteScope` endpoint now uses the `scope.value_owner_address` differently [#2137](https://github.com/provenance-io/provenance/issues/2137). + If it is empty, it indicates that there is no change to the value owner of the scope and the releated lookups and validation + are skipped. If it isn't empty, the current value owner will be looked up and the coin for the scope will be transferred to + the provided address (assuming signer validation passed). +* An authz grant on `MsgWriteScope` no longer also applies to the `UpdateValueOwners` or `MigrateValueOwner` endpoints [#2137](https://github.com/provenance-io/provenance/issues/2137). diff --git a/.changelog/unreleased/features/2137-bank-scope-value-owners.md b/.changelog/unreleased/features/2137-bank-scope-value-owners.md new file mode 100644 index 0000000000..13198d7d9f --- /dev/null +++ b/.changelog/unreleased/features/2137-bank-scope-value-owners.md @@ -0,0 +1 @@ +* Create the `viridian` upgrade. [#2137](https://github.com/provenance-io/provenance/issues/2137). diff --git a/.changelog/unreleased/improvements/2137-bank-scope-value-owners.md b/.changelog/unreleased/improvements/2137-bank-scope-value-owners.md new file mode 100644 index 0000000000..95be7d545b --- /dev/null +++ b/.changelog/unreleased/improvements/2137-bank-scope-value-owners.md @@ -0,0 +1 @@ +* Use the bank module to keep track of the value owner of scopes. [#2137](https://github.com/provenance-io/provenance/issues/2137). diff --git a/.golangci.yml b/.golangci.yml index fb2b580fcb..5cf123f1a3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -85,6 +85,7 @@ linters-settings: - cosmossdk.io/api - cosmossdk.io/client/v2/autocli + - cosmossdk.io/collections - cosmossdk.io/core - cosmossdk.io/errors - cosmossdk.io/log diff --git a/app/app.go b/app/app.go index ce7c4a1329..8e60ecc5f7 100644 --- a/app/app.go +++ b/app/app.go @@ -208,6 +208,7 @@ var ( wasmtypes.ModuleName: {authtypes.Burner}, triggertypes.ModuleName: nil, oracletypes.ModuleName: nil, + metadatatypes.ModuleName: {authtypes.Minter, authtypes.Burner}, } ) @@ -580,7 +581,7 @@ func New( ) app.MetadataKeeper = metadatakeeper.NewKeeper( - appCodec, keys[metadatatypes.StoreKey], app.AccountKeeper, app.AuthzKeeper, app.AttributeKeeper, app.MarkerKeeper, + appCodec, keys[metadatatypes.StoreKey], app.AccountKeeper, app.AuthzKeeper, app.AttributeKeeper, app.MarkerKeeper, app.BankKeeper, ) app.HoldKeeper = holdkeeper.NewKeeper( @@ -590,6 +591,7 @@ func New( app.ExchangeKeeper = exchangekeeper.NewKeeper( appCodec, keys[exchange.StoreKey], authtypes.FeeCollectorName, app.AccountKeeper, app.AttributeKeeper, app.BankKeeper, app.HoldKeeper, app.MarkerKeeper, + app.MetadataKeeper, ) pioMessageRouter := MessageRouterFunc(func(msg sdk.Msg) baseapp.MsgServiceHandler { @@ -1312,15 +1314,6 @@ func RegisterSwaggerAPI(_ client.Context, rtr *mux.Router, swaggerEnabled bool) return nil } -// GetMaccPerms returns a copy of the module account permissions -func GetMaccPerms() map[string][]string { - dupMaccPerms := make(map[string][]string) - for k, v := range maccPerms { - dupMaccPerms[k] = v - } - return dupMaccPerms -} - // initParamsKeeper init params keeper and its subspaces func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino, key, tkey storetypes.StoreKey) paramskeeper.Keeper { paramsKeeper := paramskeeper.NewKeeper(appCodec, legacyAmino, key, tkey) diff --git a/app/app_test.go b/app/app_test.go index cfe16cc61b..737fc17a71 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -70,11 +70,6 @@ func TestSimAppExportAndBlockedAddrs(t *testing.T) { require.NoError(t, err, "ExportAppStateAndValidators at zero height") } -func TestGetMaccPerms(t *testing.T) { - dup := GetMaccPerms() - require.Equal(t, maccPerms, dup, "duplicated module account permissions differed from actual module account permissions") -} - func TestExportAppStateAndValidators(t *testing.T) { opts := SetupOptions{ Logger: log.NewTestLogger(t), diff --git a/app/upgrades.go b/app/upgrades.go index 2b3b1b16c9..61240cce33 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -161,6 +161,32 @@ var upgrades = map[string]appUpgrade{ return vm, nil }, }, + "viridian-rc1": { // upgrade for v1.20.0-rc1 + Handler: func(ctx sdk.Context, app *App, vm module.VersionMap) (module.VersionMap, error) { + var err error + if err = pruneIBCExpiredConsensusStates(ctx, app); err != nil { + return nil, err + } + if vm, err = runModuleMigrations(ctx, app, vm); err != nil { + return nil, err + } + removeInactiveValidatorDelegations(ctx, app) + return vm, nil + }, + }, + "viridian": { // upgrade for v1.20.0 + Handler: func(ctx sdk.Context, app *App, vm module.VersionMap) (module.VersionMap, error) { + var err error + if err = pruneIBCExpiredConsensusStates(ctx, app); err != nil { + return nil, err + } + if vm, err = runModuleMigrations(ctx, app, vm); err != nil { + return nil, err + } + removeInactiveValidatorDelegations(ctx, app) + return vm, nil + }, + }, // TODO - Add new upgrade definitions here. } diff --git a/app/upgrades_test.go b/app/upgrades_test.go index b8f01d74eb..c292bc5bfc 100644 --- a/app/upgrades_test.go +++ b/app/upgrades_test.go @@ -29,6 +29,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" internalsdk "github.com/provenance-io/provenance/internal/sdk" + metadatakeeper "github.com/provenance-io/provenance/x/metadata/keeper" metadatatypes "github.com/provenance-io/provenance/x/metadata/types" ) @@ -1022,3 +1023,236 @@ func (s *UpgradeTestSuite) TestExecuteStoreCodeMsg() { }) } } + +func (s *UpgradeTestSuite) TestViridianRC1() { + expInLog := []string{ + "INF Pruning expired consensus states for IBC.", + "INF Starting module migrations. This may take a significant amount of time to complete. Do not restart node.", + "INF Removing inactive validator delegations.", + } + s.AssertUpgradeHandlerLogs("viridian-rc1", expInLog, nil) +} + +func (s *UpgradeTestSuite) TestViridian() { + expInLog := []string{ + "INF Pruning expired consensus states for IBC.", + "INF Starting module migrations. This may take a significant amount of time to complete. Do not restart node.", + "INF Removing inactive validator delegations.", + } + s.AssertUpgradeHandlerLogs("viridian", expInLog, nil) +} + +func (s *UpgradeTestSuite) TestMetadataMigration() { + // TODO: Delete this test with the rest of the viridian stuff. + newAddr := func(name string) sdk.AccAddress { + switch { + case len(name) < 20: + // If it's less than 19 bytes long, pad it to 20 chars. + return sdk.AccAddress(name + strings.Repeat("_", 20-len(name))) + case len(name) > 20 && len(name) < 32: + // If it's 21 to 31 bytes long, pad it to 32 chars. + return sdk.AccAddress(name + strings.Repeat("_", 32-len(name))) + } + // If the name is exactly 20 long already, or longer than 32, don't include any padding. + return sdk.AccAddress(name) + } + addrs := make([]string, 1000) + for i := range addrs { + addrs[i] = newAddr(fmt.Sprintf("%03d", i)).String() + } + + newUUID := func(i int) uuid.UUID { + // Sixteen 9's is the largest number we can handle; one more and it's 17 digits. + s.Require().LessOrEqual(i, 9999999999999999, "value provided to newScopeID") + str := fmt.Sprintf("________________%d", i) + str = str[len(str)-16:] + rv, err := uuid.FromBytes([]byte(str)) + s.Require().NoError(err, "uuid.FromBytes([]byte(%q))", str) + return rv + } + newScopeID := func(i int) metadatatypes.MetadataAddress { + return metadatatypes.ScopeMetadataAddress(newUUID(i)) + } + newSpecID := func(i int) metadatatypes.MetadataAddress { + // The spec id shouldn't really matter in here, but I want it different from a scope's index. + // So I do some math to make it seem kind of random, but is still deterministic. + // 7, 39, and 79 were picked randomly and have no special meaning. + // 4,999 was chosen so that there's 3,333 possible results. + // This will overflow at 2,097,112, but it shouldn't get bigger than 300,000 in here, so we ignore that. + j := ((i + 7) * (i + 39) * (i + 79)) % 49_999 + return metadatatypes.ScopeSpecMetadataAddress(newUUID(j)) + } + newScope := func(i int) metadatatypes.Scope { + rv := metadatatypes.Scope{ + ScopeId: newScopeID(i), + SpecificationId: newSpecID(i), + RequirePartyRollup: i%6 > 2, // 50% chance, three false, then three true, repeated. + } + + // 1 in 7 does not have a value owner. + incVO := i%7 != 0 + if incVO { + rv.ValueOwnerAddress = addrs[i%len(addrs)] + } + + // Include 1 to 5 owners and make one of them the value owner in just under 1 in 11 scopes. + ownerCount := (i % 5) + 1 + incVOInOwners := incVO && i%11 == 0 + incVOAt := i % ownerCount + rv.Owners = make([]metadatatypes.Party, ownerCount) + for o := range rv.Owners { + a := i + (o+1)*300 + if incVOInOwners && o == incVOAt { + a = i + } + rv.Owners[o].Address = addrs[a%len(addrs)] + rv.Owners[o].Role = metadatatypes.PartyType(1 + (i+o)%11) // 11 different roles, 1 to 11. + } + + daCount := (i % 3) // 0 to 2. + for d := 0; d < daCount; d++ { + a := i + (d+1)*33 + rv.DataAccess = append(rv.DataAccess, addrs[a%len(addrs)]) + } + + return rv + } + voInOwners := func(scope metadatatypes.Scope) bool { + if len(scope.ValueOwnerAddress) == 0 { + return false + } + for _, owner := range scope.Owners { + if owner.Address == scope.ValueOwnerAddress { + return true + } + } + return false + } + + // Create 300,005 scopes and write them to state. + // 6 of 7 have a value owner = 257_147 = 300_005 * 6 / 7 (truncated). + // 1 of 7 do not = 42_858 = 300_005 - 257_147 . + // There's 23,377 scopes that have the value owner in owners. + // So we expect 490,917 keys to delete = 257,147 * 2 - 23,377. + expCoin := make([]metadatatypes.MetadataAddress, 0, 257_147) + expNoCoin := make([]metadatatypes.MetadataAddress, 0, 42_858) + expDelInds := make([][]byte, 0, 490_917) + expBals := make(map[string]sdk.Coins) + t1 := time.Now() + for i := 0; i < 300_005; i++ { + scope := newScope(i) + s.Require().NoError(s.app.MetadataKeeper.V3WriteNewScope(s.ctx, scope), "[%d]: V3WriteNewScope", i) + + if len(scope.ValueOwnerAddress) == 0 { + expNoCoin = append(expNoCoin, scope.ScopeId) + continue + } + + expCoin = append(expCoin, scope.ScopeId) + + vo := scope.ValueOwnerAddress + voAddr := sdk.MustAccAddressFromBech32(vo) + expDelInds = append(expDelInds, metadatakeeper.GetValueOwnerScopeCacheKey(voAddr, scope.ScopeId)) + if !voInOwners(scope) { + expDelInds = append(expDelInds, metadatatypes.GetAddressScopeCacheKey(voAddr, scope.ScopeId)) + } + + expBals[vo] = expBals[vo].Add(scope.ScopeId.Coin()) + } + t2 := time.Now() + s.T().Logf("setup took %s", t2.Sub(t1)) + s.T().Logf("len(expDelInds) = %d", len(expDelInds)) + s.T().Logf("len(expCoin) = %d", len(expCoin)) + s.T().Logf("len(expNoCoin) = %d", len(expNoCoin)) + + mdStore := s.ctx.KVStore(s.app.GetKey(metadatatypes.ModuleName)) + for _, ind := range expDelInds { + has := mdStore.Has(ind) + s.Assert().True(has, "mdStore.Has(%v) before running the migrations", ind) + } + mdStore = nil + + expLogs := []string{ + "INF Starting module migrations. This may take a significant amount of time to complete. Do not restart node.", + "INF Starting migration of x/metadata from 3 to 4. module=x/metadata", + "INF Moving scope value owner data into x/bank ledger. module=x/metadata", + "INF Progress update: module=x/metadata scopes=10000 value owners=8571", + "INF Progress update: module=x/metadata scopes=20000 value owners=17143", + "INF Progress update: module=x/metadata scopes=30000 value owners=25714", + "INF Progress update: module=x/metadata scopes=40000 value owners=34286", + "INF Progress update: module=x/metadata scopes=50000 value owners=42857", + "INF Progress update: module=x/metadata scopes=60000 value owners=51428", + "INF Progress update: module=x/metadata scopes=70000 value owners=60000", + "INF Progress update: module=x/metadata scopes=80000 value owners=68571", + "INF Progress update: module=x/metadata scopes=90000 value owners=77143", + "INF Progress update: module=x/metadata scopes=100000 value owners=85714", + "INF Progress update: module=x/metadata scopes=110000 value owners=94286", + "INF Progress update: module=x/metadata scopes=120000 value owners=102857", + "INF Progress update: module=x/metadata scopes=130000 value owners=111428", + "INF Progress update: module=x/metadata scopes=140000 value owners=120000", + "INF Progress update: module=x/metadata scopes=150000 value owners=128571", + "INF Progress update: module=x/metadata scopes=160000 value owners=137143", + "INF Progress update: module=x/metadata scopes=170000 value owners=145714", + "INF Progress update: module=x/metadata scopes=180000 value owners=154286", + "INF Progress update: module=x/metadata scopes=190000 value owners=162857", + "INF Progress update: module=x/metadata scopes=200000 value owners=171428", + "INF Progress update: module=x/metadata scopes=210000 value owners=180000", + "INF Progress update: module=x/metadata scopes=220000 value owners=188572", + "INF Progress update: module=x/metadata scopes=230000 value owners=197143", + "INF Progress update: module=x/metadata scopes=240000 value owners=205714", + "INF Progress update: module=x/metadata scopes=250000 value owners=214286", + "INF Progress update: module=x/metadata scopes=260000 value owners=222857", + "INF Progress update: module=x/metadata scopes=270000 value owners=231429", + "INF Progress update: module=x/metadata scopes=280000 value owners=240000", + "INF Progress update: module=x/metadata scopes=290000 value owners=248572", + "INF Progress update: module=x/metadata scopes=300000 value owners=257143", + "INF Done moving scope value owners into bank module. module=x/metadata scopes=300005 value owners=257147", + "INF Done migrating x/metadata from 3 to 4. module=x/metadata", + "INF Module migrations completed.", + } + + vm, err := s.app.UpgradeKeeper.GetModuleVersionMap(s.ctx) + s.Require().NoError(err, "GetModuleVersionMap") + s.Require().Equal(4, int(vm[metadatatypes.ModuleName]), "%s module version", metadatatypes.ModuleName) + // Drop it back to 3 so the migration runs. + vm[metadatatypes.ModuleName] = 3 + + runner := func() { + t1 = time.Now() + vm, err = runModuleMigrations(s.ctx, s.app, vm) + t2 = time.Now() + } + s.ExecuteAndAssertLogs(runner, expLogs, nil, true, "runModuleMigrations") + s.Assert().NoError(err, "error from runModuleMigrations") + s.Assert().Equal(4, int(vm[metadatatypes.ModuleName]), "vm[metadatatypes.ModuleName]") + s.T().Logf("runModuleMigrations took %s", t2.Sub(t1)) + + for _, scopeID := range expCoin { + denom := scopeID.Denom() + supply := s.app.BankKeeper.GetSupply(s.ctx, denom) + s.Assert().Equal("1"+denom, supply.String(), "GetSupply(%q)", denom) + } + + for _, scopeID := range expNoCoin { + denom := scopeID.Denom() + supply := s.app.BankKeeper.GetSupply(s.ctx, denom) + s.Assert().Equal("0"+denom, supply.String(), "GetSupply(%q)", denom) + } + + for i, addr := range addrs { + accAddr := sdk.MustAccAddressFromBech32(addr) + actBal := s.app.BankKeeper.GetAllBalances(s.ctx, accAddr) + for _, expCoin := range expBals[addr] { + found, actCoin := actBal.Find(expCoin.Denom) + if s.Assert().True(found, "[%d]%q: found bool from actBal.Find(%q)", i, addr, expCoin.Denom) { + s.Assert().Equal(expCoin.String(), actCoin.String(), "[%d]%q: balance coin for %q", i, addr, expCoin.Denom) + } + } + } + + mdStore = s.ctx.KVStore(s.app.GetKey(metadatatypes.ModuleName)) + for _, ind := range expDelInds { + has := mdStore.Has(ind) + s.Assert().False(has, "mdStore.Has(%v) after running the migrations", ind) + } +} diff --git a/client/docs/swagger-ui/swagger.yaml b/client/docs/swagger-ui/swagger.yaml index 72da985d2d..d2295a698e 100644 --- a/client/docs/swagger-ui/swagger.yaml +++ b/client/docs/swagger-ui/swagger.yaml @@ -14076,11 +14076,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with this - scope. Standard blockchain accounts and marker accounts + The address that controls the value associated with this + scope. - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -17149,12 +17164,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -17996,12 +18025,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -18849,12 +18892,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -20859,12 +20916,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -21697,12 +21768,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -22569,12 +22654,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -23430,12 +23529,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -24302,12 +24415,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -25163,12 +25290,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -26020,12 +26161,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -26892,12 +27047,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -27705,13 +27874,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. - are supported for this value. This attribute may - only be changed by the entity indicated once it is - set. + + The value owner is actually tracked by the bank + module using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -29002,12 +29184,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -29855,12 +30051,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -30716,12 +30926,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". + + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -31573,12 +31797,26 @@ paths: value_owner_address: type: string description: >- - An address that controls the value associated with - this scope. Standard blockchain accounts and marker - accounts + The address that controls the value associated with + this scope. + + + The value owner is actually tracked by the bank module + using a coin with the denom "nft/". - are supported for this value. This attribute may only - be changed by the entity indicated once it is set. + The value owner can be changed using WriteScope or + anything that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -79770,11 +80008,25 @@ definitions: value_owner_address: type: string description: >- - An address that controls the value associated with this scope. - Standard blockchain accounts and marker accounts + The address that controls the value associated with this scope. + + + The value owner is actually tracked by the bank module using a + coin with the denom "nft/". + + The value owner can be changed using WriteScope or anything that + transfers funds, e.g. MsgSend. + - are supported for this value. This attribute may only be - changed by the entity indicated once it is set. + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -83406,11 +83658,26 @@ definitions: value_owner_address: type: string description: >- - An address that controls the value associated with this - scope. Standard blockchain accounts and marker accounts + The address that controls the value associated with this + scope. - are supported for this value. This attribute may only be - changed by the entity indicated once it is set. + + The value owner is actually tracked by the bank module using a + coin with the denom "nft/". + + The value owner can be changed using WriteScope or anything + that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -84087,11 +84354,25 @@ definitions: value_owner_address: type: string description: >- - An address that controls the value associated with this scope. - Standard blockchain accounts and marker accounts + The address that controls the value associated with this scope. + + + The value owner is actually tracked by the bank module using a coin + with the denom "nft/". - are supported for this value. This attribute may only be changed by - the entity indicated once it is set. + The value owner can be changed using WriteScope or anything that + transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -84250,11 +84531,26 @@ definitions: value_owner_address: type: string description: >- - An address that controls the value associated with this - scope. Standard blockchain accounts and marker accounts + The address that controls the value associated with this + scope. + + + The value owner is actually tracked by the bank module using a + coin with the denom "nft/". + + The value owner can be changed using WriteScope or anything + that transfers funds, e.g. MsgSend. + - are supported for this value. This attribute may only be - changed by the entity indicated once it is set. + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -85913,11 +86209,25 @@ definitions: value_owner_address: type: string description: >- - An address that controls the value associated with this scope. - Standard blockchain accounts and marker accounts + The address that controls the value associated with this scope. - are supported for this value. This attribute may only be changed - by the entity indicated once it is set. + + The value owner is actually tracked by the bank module using a + coin with the denom "nft/". + + The value owner can be changed using WriteScope or anything that + transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -86130,11 +86440,26 @@ definitions: value_owner_address: type: string description: >- - An address that controls the value associated with this - scope. Standard blockchain accounts and marker accounts + The address that controls the value associated with this + scope. + + + The value owner is actually tracked by the bank module using + a coin with the denom "nft/". - are supported for this value. This attribute may only be - changed by the entity indicated once it is set. + The value owner can be changed using WriteScope or anything + that transfers funds, e.g. MsgSend. + + + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- @@ -87166,11 +87491,26 @@ definitions: value_owner_address: type: string description: >- - An address that controls the value associated with this - scope. Standard blockchain accounts and marker accounts + The address that controls the value associated with this + scope. + + + The value owner is actually tracked by the bank module using a + coin with the denom "nft/". + + The value owner can be changed using WriteScope or anything + that transfers funds, e.g. MsgSend. + - are supported for this value. This attribute may only be - changed by the entity indicated once it is set. + During WriteScope: + - If this field is empty, it indicates that there should not be a change to the value owner. + I.e. Once a scope has a value owner, it will always have one (until it's deleted). + - If this field has a value, the existing value owner will be looked up, and + - If there's already an existing value owner, they must be a signer, + and the coin will be transferred to the new value owner. + - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + If the scope already exists, the owners must be signers (just like changing other fields). + If it's a new scope, there's no special signer limitations related to the value owner. require_party_rollup: type: boolean description: >- diff --git a/docs/proto-docs.md b/docs/proto-docs.md index 58ad38a9a8..32bd11c3f8 100644 --- a/docs/proto-docs.md +++ b/docs/proto-docs.md @@ -10734,7 +10734,7 @@ Scope defines a root reference for a collection of records owned by one or more | `specification_id` | [bytes](#bytes) | | the scope specification that contains the specifications for data elements allowed within this scope | | `owners` | [Party](#provenance-metadata-v1-Party) | repeated | These parties represent top level owners of the records within. These parties must sign any requests that modify the data within the scope. These addresses are in union with parties listed on the sessions. | | `data_access` | [string](#string) | repeated | Addresses in this list are authorized to receive off-chain data associated with this scope. | -| `value_owner_address` | [string](#string) | | An address that controls the value associated with this scope. Standard blockchain accounts and marker accounts are supported for this value. This attribute may only be changed by the entity indicated once it is set. | +| `value_owner_address` | [string](#string) | | The address that controls the value associated with this scope.
The value owner is actually tracked by the bank module using a coin with the denom "nft/". The value owner can be changed using WriteScope or anything that transfers funds, e.g. MsgSend.
During WriteScope: - If this field is empty, it indicates that there should not be a change to the value owner. I.e. Once a scope has a value owner, it will always have one (until it's deleted). - If this field has a value, the existing value owner will be looked up, and - If there's already an existing value owner, they must be a signer, and the coin will be transferred to the new value owner. - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. If the scope already exists, the owners must be signers (just like changing other fields). If it's a new scope, there's no special signer limitations related to the value owner. | | `require_party_rollup` | [bool](#bool) | | Whether all parties in this scope and its sessions must be present in this scope's owners field. This also enables use of optional=true scope owners and session parties. | diff --git a/internal/provutils/pair.go b/internal/provutils/pair.go new file mode 100644 index 0000000000..25d49eb433 --- /dev/null +++ b/internal/provutils/pair.go @@ -0,0 +1,47 @@ +package provutils + +import "fmt" + +// Pair is a struct with two things. +type Pair[KA any, KB any] struct { + A KA + B KB +} + +// NewPair creates a new Pair containing the provided values. +func NewPair[KA any, KB any](a KA, b KB) *Pair[KA, KB] { + return &Pair[KA, KB]{A: a, B: b} +} + +// SetA updates this Pair to have the provided value in A. +func (p *Pair[KA, KB]) SetA(a KA) { + p.A = a +} + +// GetA returns the A value in this Pair. +func (p *Pair[KA, KB]) GetA() KA { + return p.A +} + +// SetB updates this Pair to have the provided value in B. +func (p *Pair[KA, KB]) SetB(b KB) { + p.B = b +} + +// GetB returns the B value in this Pair. +func (p *Pair[KA, KB]) GetB() KB { + return p.B +} + +// Values returns both of the values in this Pair: A, B. +func (p *Pair[KA, KB]) Values() (KA, KB) { + return p.A, p.B +} + +// String returns a string representation of this Pair. +func (p *Pair[KA, KB]) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("<%v:%v>", p.A, p.B) +} diff --git a/internal/provutils/pair_test.go b/internal/provutils/pair_test.go new file mode 100644 index 0000000000..9b43660804 --- /dev/null +++ b/internal/provutils/pair_test.go @@ -0,0 +1,163 @@ +package provutils + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// pairTestCase is a struct that defines a test case to run involving the Pair type. +type pairTestCase[KA any, KB any] struct { + // A is the A value to give the initial pair. + A KA + // B is the B value to give the initial pair. + B KB + // ExpStr is the expected value of the initial pair.String(). + ExpStr string + + // NewA is a pointer to an A value to provide to SetA. Leave nil to not call SetA. + NewA *KA + // NewB is a pointer to a B value to provide to SetB. Leave nil to not call SetB. + NewB *KB + // ExpNewStr is the expected value of pair.String() after calling SetA and/or SetB. + ExpNewStr string +} + +// runPairTest runs a subtest that creates a pair and makes sure it behaves as expected. +func runPairTest[KA any, KB any](t *testing.T, tc pairTestCase[KA, KB]) bool { + t.Helper() + name := tc.ExpStr + if len(tc.ExpNewStr) > 0 { + name += " to " + tc.ExpNewStr + } + rv := t.Run(name, func(t *testing.T) { + var pair *Pair[KA, KB] + testNewPair := func() { + pair = NewPair(tc.A, tc.B) + } + require.NotPanics(t, testNewPair, "NewPair") + require.NotNil(t, pair, "NewPair result") + + assertPair(t, pair, tc.A, tc.B, tc.ExpStr, "") + + if tc.NewA == nil && tc.NewB == nil { + return // No desired changes, so no reason to do the rest. + } + + expA, expB := tc.A, tc.B + setAOK, setBOK := true, true + if tc.NewA != nil { + expA = *tc.NewA + testSetA := func() { + pair.SetA(expA) + } + setAOK = assert.NotPanics(t, testSetA, "SetA(...)") + } + if tc.NewB != nil { + expB = *tc.NewB + testSetB := func() { + pair.SetB(expB) + } + setBOK = assert.NotPanics(t, testSetB, "SetB(...)") + } + + if setAOK && setBOK { + assertPair(t, pair, expA, expB, tc.ExpNewStr, " after being changed") + } + }) + return rv +} + +// assertPair runs several assertions against the provided pair, ensuring it is and behaves as expected. +// Returns true if it passes all assertions, false if there's something wrong. +func assertPair[KA any, KB any](t *testing.T, pair *Pair[KA, KB], expA KA, expB KB, expStr string, msgSuffix string) { + t.Helper() + assert.Equal(t, expA, pair.A, "A field%s", msgSuffix) + assert.Equal(t, expB, pair.B, "B field%s", msgSuffix) + + var actA KA + testGetA := func() { + actA = pair.GetA() + } + if assert.NotPanics(t, testGetA, "GetA()%s", msgSuffix) { + assert.Equal(t, expA, actA, "GetA() result%s", msgSuffix) + } + + var actB KB + testGetB := func() { + actB = pair.GetB() + } + if assert.NotPanics(t, testGetB, "GetB()%s", msgSuffix) { + assert.Equal(t, expB, actB, "GetB() result%s", msgSuffix) + } + + var actString string + testString := func() { + actString = pair.String() + } + if assert.NotPanics(t, testString, "String()%s", msgSuffix) { + assert.Equal(t, expStr, actString, "String() result%s", msgSuffix) + } + + var actAV KA + var actBV KB + testValues := func() { + actAV, actBV = pair.Values() + } + if assert.NotPanics(t, testValues, "Values()%s", msgSuffix) { + assert.Equal(t, expA, actAV, "A value returned from Values()%s", msgSuffix) + assert.Equal(t, expB, actBV, "B value returned from Values()%s", msgSuffix) + } +} + +// ptrTo returns a pointer to the provided value. It's like & but this works on built-in data types. +func ptrTo[V any](v V) *V { + return &v +} + +// testType is just a simple dumb struct that I can use to test some pair stuff. +type testType struct { + Id string +} + +// newTT creates a new testType with the given id. +func newTT(id string) *testType { + return &testType{Id: id} +} + +// String implements the fmt.Stringer interface so that I know what %v looks like for a testType. +func (t *testType) String() string { + if t == nil { + return "" + } + return fmt.Sprintf("%T=%s", t, t.Id) +} + +func TestPair(t *testing.T) { + tests := []pairTestCase[int, string]{ + {A: 0, B: "", ExpStr: "<0:>", NewA: ptrTo(1), NewB: ptrTo("one"), ExpNewStr: "<1:one>"}, + {A: 1, B: "two", ExpStr: "<1:two>", NewA: ptrTo(555), NewB: ptrTo("seven"), ExpNewStr: "<555:seven>"}, + {A: 33, B: "three", ExpStr: "<33:three>", NewA: ptrTo(0), NewB: nil, ExpNewStr: "<0:three>"}, + {A: 4321, B: "four", ExpStr: "<4321:four>", NewA: nil, NewB: ptrTo(""), ExpNewStr: "<4321:>"}, + } + + for _, tc := range tests { + runPairTest(t, tc) + } + + runPairTest(t, pairTestCase[string, *testType]{A: "one", B: newTT("ONE"), ExpStr: ""}) + runPairTest(t, pairTestCase[*testType, string]{A: newTT("two"), B: "TWO", ExpStr: "<*provutils.testType=two:TWO>"}) + runPairTest(t, pairTestCase[*testType, *testType]{ + A: newTT("three"), B: newTT("FOUR"), ExpStr: "<*provutils.testType=three:*provutils.testType=FOUR>", + NewA: ptrTo(newTT("tHrEe")), NewB: ptrTo(newTT("FoUr")), ExpNewStr: "<*provutils.testType=tHrEe:*provutils.testType=FoUr>", + }) + runPairTest(t, pairTestCase[*testType, *testType]{ + A: newTT("five"), B: nil, ExpStr: "<*provutils.testType=five:>", + NewA: ptrTo((*testType)(nil)), NewB: ptrTo(newTT("sIx")), ExpNewStr: "<:*provutils.testType=sIx>", + }) + runPairTest(t, pairTestCase[[]byte, string]{ + A: []byte{108, 101, 102, 116}, B: "right", ExpStr: "<[108 101 102 116]:right>", + }) +} diff --git a/internal/provutils/slices.go b/internal/provutils/slices.go new file mode 100644 index 0000000000..03f88ca0ec --- /dev/null +++ b/internal/provutils/slices.go @@ -0,0 +1,29 @@ +package provutils + +// FindMissing returns all elements of the required list that are not found in the entries list. +// Duplicate entries in required do not require duplicate entries in toCheck. +// E.g. FindMissing([a, b, a], [a]) => [b], and FindMissing([a, b, a], [b]) => [a, a]. +// +// See also: FindMissingFunc. +func FindMissing[S ~[]E, E comparable](required, toCheck S) S { + return FindMissingFunc(required, toCheck, func(r, c E) bool { return r == c }) +} + +// FindMissingFunc returns all entries in required where the equals function returns false for all entries of toCheck. +// Duplicate entries in required do not require duplicate entries in toCheck. +// E.g. FindMissingFunc([a, b, a], [a]) => [b], and FindMissingFunc([a, b, a], [b]) => [a, a]. +// +// See also: FindMissing. +func FindMissingFunc[SR ~[]R, SC ~[]C, R any, C any](required SR, toCheck SC, equals func(R, C) bool) SR { + var rv []R +reqLoop: + for _, req := range required { + for _, entry := range toCheck { + if equals(req, entry) { + continue reqLoop + } + } + rv = append(rv, req) + } + return rv +} diff --git a/internal/provutils/slices_test.go b/internal/provutils/slices_test.go new file mode 100644 index 0000000000..314144f65d --- /dev/null +++ b/internal/provutils/slices_test.go @@ -0,0 +1,470 @@ +package provutils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// stringSame is a string with an IsSameAs(stringSame) function. +type stringSame string + +// IsSameAs returns true if this stringSame is the same as the provided one. +func (s stringSame) IsSameAs(c stringSame) bool { + return string(s) == string(c) +} + +// newStringSames converts a slice of strings to a slice of stringEqs. +// nil in => nil out. empty in => empty out. +func newStringSames(strs []string) []stringSame { + if strs == nil { + return nil + } + rv := make([]stringSame, len(strs), cap(strs)) + for i, str := range strs { + rv[i] = stringSame(str) + } + return rv +} + +// stringSameR is a string with an IsSameAs(stringSameC) function (note the other type there). +type stringSameR string + +// stringSameC is a string that can be provided to the stringSameR IsSameAs function. +type stringSameC string + +// IsSameAs returns true if this stringSameC is the same as the provided stringSameC. +func (s stringSameR) IsSameAs(c stringSameC) bool { + return string(s) == string(c) +} + +// newStringSameRs converts a slice of strings to a slice of stringEqRs. +// nil in => nil out. empty in => empty out. +func newStringSameRs(strs []string) []stringSameR { + if strs == nil { + return nil + } + rv := make([]stringSameR, len(strs), cap(strs)) + for i, str := range strs { + rv[i] = stringSameR(str) + } + return rv +} + +// newStringSameCs converts a slice of strings to a slice of stringEqCs. +// nil in => nil out. empty in => empty out. +func newStringSameCs(strs []string) []stringSameC { + if strs == nil { + return nil + } + rv := make([]stringSameC, len(strs), cap(strs)) + for i, str := range strs { + rv[i] = stringSameC(str) + } + return rv +} + +type testCaseFindMissing struct { + name string + required []string + toCheck []string + expected []string +} + +func testCasesForFindMissing() []testCaseFindMissing { + return []testCaseFindMissing{ + { + name: "nil required - nil toCheck - nil out", + required: nil, + toCheck: nil, + expected: nil, + }, + { + name: "empty required - nil toCheck - nil out", + required: []string{}, + toCheck: nil, + expected: nil, + }, + { + name: "nil required - empty toCheck - nil out", + required: nil, + toCheck: []string{}, + expected: nil, + }, + { + name: "empty required - empty toCheck - nil out", + required: []string{}, + toCheck: []string{}, + expected: nil, + }, + { + name: "nil required - 2 toCheck - nil out", + required: nil, + toCheck: []string{"one", "two"}, + expected: nil, + }, + { + name: "empty required - 2 toCheck - nil out", + required: []string{}, + toCheck: []string{"one", "two"}, + expected: nil, + }, + { + name: "1 required - is only toCheck - nil out", + required: []string{"one"}, + toCheck: []string{"one"}, + expected: nil, + }, + { + name: "1 required - is 1st of 2 toCheck - nil out", + required: []string{"one"}, + toCheck: []string{"one", "two"}, + expected: nil, + }, + { + name: "1 required - is 2nd of 2 toCheck - nil out", + required: []string{"one"}, + toCheck: []string{"two", "one"}, + expected: nil, + }, + { + name: "1 required - nil toCheck - required out", + required: []string{"one"}, + toCheck: nil, + expected: []string{"one"}, + }, + { + name: "1 required - empty toCheck - required out", + required: []string{"one"}, + toCheck: []string{}, + expected: []string{"one"}, + }, + { + name: "1 required - 1 other in toCheck - required out", + required: []string{"one"}, + toCheck: []string{"two"}, + expected: []string{"one"}, + }, + { + name: "1 required - 2 other in toCheck - required out", + required: []string{"one"}, + toCheck: []string{"two", "three"}, + expected: []string{"one"}, + }, + { + name: "2 required - both in toCheck - nil out", + required: []string{"one", "two"}, + toCheck: []string{"one", "two"}, + expected: nil, + }, + { + name: "2 required - reversed in toCheck - nil out", + required: []string{"one", "two"}, + toCheck: []string{"two", "one"}, + expected: nil, + }, + { + name: "2 required - only 1st in toCheck - 2nd out", + required: []string{"one", "two"}, + toCheck: []string{"one"}, + expected: []string{"two"}, + }, + { + name: "2 required - only 2nd in toCheck - 1st out", + required: []string{"one", "two"}, + toCheck: []string{"two"}, + expected: []string{"one"}, + }, + { + name: "2 required - 1st and other in toCheck - 2nd out", + required: []string{"one", "two"}, + toCheck: []string{"one", "other"}, + expected: []string{"two"}, + }, + { + name: "2 required - 2nd and other in toCheck - 1st out", + required: []string{"one", "two"}, + toCheck: []string{"two", "other"}, + expected: []string{"one"}, + }, + { + name: "2 required - nil toCheck - required out", + required: []string{"one", "two"}, + toCheck: nil, + expected: []string{"one", "two"}, + }, + { + name: "2 required - empty toCheck - required out", + required: []string{"one", "two"}, + toCheck: []string{}, + expected: []string{"one", "two"}, + }, + { + name: "2 required - neither in 1 toCheck - required out", + required: []string{"one", "two"}, + toCheck: []string{"neither"}, + expected: []string{"one", "two"}, + }, + { + name: "2 required - neither in 3 toCheck - required out", + required: []string{"one", "two"}, + toCheck: []string{"neither", "nor", "nothing"}, + expected: []string{"one", "two"}, + }, + { + name: "2 required - 1st not in 3 toCheck 2nd at 0 - 1st out", + required: []string{"one", "two"}, + toCheck: []string{"two", "nor", "nothing"}, + expected: []string{"one"}, + }, + { + name: "2 required - 1st not in 3 toCheck 2nd at 1 - 1st out", + required: []string{"one", "two"}, + toCheck: []string{"neither", "two", "nothing"}, + expected: []string{"one"}, + }, + { + name: "2 required - 1st not in 3 toCheck 2nd at 2 - 1st out", + required: []string{"one", "two"}, + toCheck: []string{"neither", "nor", "two"}, + expected: []string{"one"}, + }, + { + name: "2 required - 2nd not in 3 toCheck 1st at 0 - 2nd out", + required: []string{"one", "two"}, + toCheck: []string{"one", "nor", "nothing"}, + expected: []string{"two"}, + }, + { + name: "2 required - 2nd not in 3 toCheck 1st at 1 - 2nd out", + required: []string{"one", "two"}, + toCheck: []string{"neither", "one", "nothing"}, + expected: []string{"two"}, + }, + { + name: "2 required - 2nd not in 3 toCheck 1st at 2 - 2nd out", + required: []string{"one", "two"}, + toCheck: []string{"neither", "nor", "one"}, + expected: []string{"two"}, + }, + + { + name: "3 required - none in 5 toCheck - required out", + required: []string{"one", "two", "three"}, + toCheck: []string{"other1", "other2", "other3", "other4", "other5"}, + expected: []string{"one", "two", "three"}, + }, + { + name: "3 required - only 1st in 5 toCheck - 2nd 3rd out", + required: []string{"one", "two", "three"}, + toCheck: []string{"other1", "other2", "one", "other4", "other5"}, + expected: []string{"two", "three"}, + }, + { + name: "3 required - only 2nd in 5 toCheck - 1st 3rd out", + required: []string{"one", "two", "three"}, + toCheck: []string{"other1", "two", "other3", "other4", "other5"}, + expected: []string{"one", "three"}, + }, + { + name: "3 required - only 3rd in 5 toCheck - 1st 2nd out", + required: []string{"one", "two", "three"}, + toCheck: []string{"other1", "other2", "other3", "three", "other5"}, + expected: []string{"one", "two"}, + }, + { + name: "3 required - 1st 2nd in 5 toCheck - 3rd out", + required: []string{"one", "two", "three"}, + toCheck: []string{"other1", "two", "other3", "one", "other5"}, + expected: []string{"three"}, + }, + { + name: "3 required - 1st 3nd in 5 toCheck - 2nd out", + required: []string{"one", "two", "three"}, + toCheck: []string{"three", "other2", "other3", "other4", "one"}, + expected: []string{"two"}, + }, + { + name: "3 required - 2nd 3rd in 5 toCheck - 1st out", + required: []string{"one", "two", "three"}, + toCheck: []string{"other1", "other2", "two", "three", "other5"}, + expected: []string{"one"}, + }, + { + name: "3 required - all in 5 toCheck - nil out", + required: []string{"one", "two", "three"}, + toCheck: []string{"two", "other2", "one", "three", "other5"}, + expected: nil, + }, + { + name: "3 required with dup - all in toCheck - nil out", + required: []string{"one", "two", "one"}, + toCheck: []string{"one", "two"}, + expected: nil, + }, + { + name: "3 required with dup - dup not in toCheck - dups out", + required: []string{"one", "two", "one"}, + toCheck: []string{"two"}, + expected: []string{"one", "one"}, + }, + { + name: "3 required with dup - other not in toCheck - other out", + required: []string{"one", "two", "one"}, + toCheck: []string{"one"}, + expected: []string{"two"}, + }, + { + name: "3 required all dup - in toCheck - nil out", + required: []string{"one", "one", "one"}, + toCheck: []string{"one"}, + expected: nil, + }, + { + name: "3 required all dup - not in toCheck - all 3 out", + required: []string{"one", "one", "one"}, + toCheck: []string{"two"}, + expected: []string{"one", "one", "one"}, + }, + } +} + +func TestFindMissing(t *testing.T) { + for _, tc := range testCasesForFindMissing() { + t.Run(tc.name, func(t *testing.T) { + actual := FindMissing(tc.required, tc.toCheck) + assert.Equal(t, tc.expected, actual, "findMissing") + }) + } +} + +func TestFindMissingFunc(t *testing.T) { + t.Run("equals equals", func(t *testing.T) { + equals := func(r, c string) bool { + return r == c + } + for _, tc := range testCasesForFindMissing() { + t.Run(tc.name, func(t *testing.T) { + actual := FindMissingFunc(tc.required, tc.toCheck, equals) + assert.Equal(t, tc.expected, actual, "FindMissingFunc") + }) + } + }) + + t.Run("is same as same types", func(t *testing.T) { + equals := func(r, c stringSame) bool { + return r.IsSameAs(c) + } + for _, tc := range testCasesForFindMissing() { + t.Run(tc.name, func(t *testing.T) { + required := newStringSames(tc.required) + toCheck := newStringSames(tc.toCheck) + expected := newStringSames(tc.expected) + actual := FindMissingFunc(required, toCheck, equals) + assert.Equal(t, expected, actual, "FindMissingFunc") + }) + } + }) + + t.Run("is same as different types", func(t *testing.T) { + equals := func(r stringSameR, c stringSameC) bool { + return r.IsSameAs(c) + } + for _, tc := range testCasesForFindMissing() { + t.Run(tc.name, func(t *testing.T) { + required := newStringSameRs(tc.required) + toCheck := newStringSameCs(tc.toCheck) + expected := newStringSameRs(tc.expected) + actual := FindMissingFunc(required, toCheck, equals) + assert.Equal(t, expected, actual, "FindMissingFunc") + }) + } + }) + + t.Run("string lengths", func(t *testing.T) { + equals := func(r string, c int) bool { + return len(r) == c + } + req := []string{"a", "bb", "ccc", "dddd", "eeeee"} + checks := []struct { + name string + toCheck []int + expected []string + }{ + {name: "all there", toCheck: []int{1, 2, 3, 4, 5}, expected: nil}, + {name: "missing len 1", toCheck: []int{2, 3, 4, 5}, expected: []string{"a"}}, + {name: "missing len 2", toCheck: []int{1, 3, 4, 5}, expected: []string{"bb"}}, + {name: "missing len 3", toCheck: []int{1, 2, 4, 5}, expected: []string{"ccc"}}, + {name: "missing len 4", toCheck: []int{1, 2, 3, 5}, expected: []string{"dddd"}}, + {name: "missing len 5", toCheck: []int{1, 2, 3, 4}, expected: []string{"eeeee"}}, + {name: "none there", toCheck: []int{0, 6}, expected: req}, + } + for _, tc := range checks { + t.Run(tc.name, func(t *testing.T) { + actual := FindMissingFunc(req, tc.toCheck, equals) + assert.Equal(t, tc.expected, actual, "FindMissingFunc equals returns true if the string is the right length") + }) + } + }) + + t.Run("div two", func(t *testing.T) { + equals := func(r int, c int) bool { + return r/2 == c + } + req := []int{1, 2, 3, 4, 5} + checks := []struct { + name string + toCheck []int + expected []int + }{ + {name: "all there", toCheck: []int{0, 1, 2}, expected: nil}, + {name: "missing 0", toCheck: []int{1, 2}, expected: []int{1}}, + {name: "missing 1", toCheck: []int{0, 2}, expected: []int{2, 3}}, + {name: "missing 2", toCheck: []int{0, 1}, expected: []int{4, 5}}, + {name: "none there", toCheck: []int{-1, 3}, expected: req}, + } + for _, tc := range checks { + t.Run(tc.name, func(t *testing.T) { + actual := FindMissingFunc(req, tc.toCheck, equals) + assert.Equal(t, tc.expected, actual, "FindMissingFunc equals returns true if r/2 == c") + }) + } + }) + + t.Run("all true", func(t *testing.T) { + equals := func(r, c string) bool { + return true + } + for _, tc := range testCasesForFindMissing() { + t.Run(tc.name, func(t *testing.T) { + var expected []string + // required entries are only marked as found after being compared to something. + // So if there's nothing in the toCheck list, all the required will be returned. + // But if tc.required is an empty slice, we still expect to get nil back, so we don't + // set expected = tc.required in that case. + if len(tc.toCheck) == 0 && len(tc.required) > 0 { + expected = tc.required + } + actual := FindMissingFunc(tc.required, tc.toCheck, equals) + assert.Equal(t, expected, actual, "FindMissingFunc equals always returns true") + }) + } + }) + + t.Run("all false", func(t *testing.T) { + equals := func(r, c string) bool { + return false + } + for _, tc := range testCasesForFindMissing() { + t.Run(tc.name, func(t *testing.T) { + // If tc.required is nil, or an empty slice, we expect nil, otherwise, we always expect tc.required back. + var expected []string + if len(tc.required) > 0 { + expected = tc.required + } + actual := FindMissingFunc(tc.required, tc.toCheck, equals) + assert.Equal(t, expected, actual, "FindMissingFunc equals always returns false") + }) + } + }) +} diff --git a/internal/provutils/ternary.go b/internal/provutils/ternary.go new file mode 100644 index 0000000000..f8f460d461 --- /dev/null +++ b/internal/provutils/ternary.go @@ -0,0 +1,29 @@ +package provutils + +// Ternary returns ifTrue if test is true, or ifFalse if test is false. +// It's similar to Ternary assignments in other languages that often +// have the syntax like this: value = test ? ifTrue : ifFalse; +func Ternary[V any](test bool, ifTrue V, ifFalse V) V { + if test { + return ifTrue + } + return ifFalse +} + +// Pluralize returns ifOne if the provided vals has length 1, otherwise returns ifOther. +// +// E.g. Pluralize(parties, "party", "parties") +// +// If the only difference between ifOne and ifOther is an additional "s", consider using PluralEnding. +func Pluralize[S ~[]E, E any](vals S, ifOne, ifOther string) string { + return Ternary(len(vals) == 1, ifOne, ifOther) +} + +// PluralEnding returns an empty string if vals has length 1, otherwise returns "s". +// +// E.g. name := "cat" + PluralEnding(cats) +// +// If you need more than the addition of an "s", use Pluralize. +func PluralEnding[S ~[]E, E any](vals S) string { + return Ternary(len(vals) == 1, "", "s") +} diff --git a/internal/provutils/ternary_test.go b/internal/provutils/ternary_test.go new file mode 100644 index 0000000000..12c9a316be --- /dev/null +++ b/internal/provutils/ternary_test.go @@ -0,0 +1,124 @@ +package provutils + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTernary(t *testing.T) { + tests := []struct { + test bool + ifTrue int + ifFalse int + exp int + }{ + {test: true, ifTrue: 0, ifFalse: 0, exp: 0}, + {test: true, ifTrue: 0, ifFalse: 1, exp: 0}, + {test: true, ifTrue: 1, ifFalse: 0, exp: 1}, + {test: true, ifTrue: -10, ifFalse: 20, exp: -10}, + {test: true, ifTrue: 100, ifFalse: -200, exp: 100}, + {test: false, ifTrue: 0, ifFalse: 0, exp: 0}, + {test: false, ifTrue: 0, ifFalse: 1, exp: 1}, + {test: false, ifTrue: 1, ifFalse: 0, exp: 0}, + {test: false, ifTrue: -10, ifFalse: 20, exp: 20}, + {test: false, ifTrue: 100, ifFalse: -200, exp: -200}, + } + + for _, tc := range tests { + name := fmt.Sprintf("%t, %d, %d", tc.test, tc.ifTrue, tc.ifFalse) + t.Run(name, func(t *testing.T) { + var actual int + testFunc := func() { + actual = Ternary(tc.test, tc.ifTrue, tc.ifFalse) + } + require.NotPanics(t, testFunc, "Ternary(%s)", name) + assert.Equal(t, tc.exp, actual, "result of Ternary(%s)", name) + }) + } +} + +func TestPluralize(t *testing.T) { + ifOne := "ifOne" + ifOther := "ifOther" + strs := []string{ + "one", "two", "three", "four", "five", + "six", "seven", "eight", "nine", "ten", + } + + tests := []struct { + name string + s []string + exp string + }{ + { + name: "nil slice", + s: nil, + exp: ifOther, + }, + { + name: "empty slice", + s: make([]string, 0), + exp: ifOther, + }, + { + name: "one entry", + s: strs[0:1], + exp: ifOne, + }, + { + name: "two entries", + s: strs[1:3], + exp: ifOther, + }, + { + name: "three entries", + s: strs[3:6], + exp: ifOther, + }, + { + name: "ten entries", + s: strs[0:10], + exp: ifOther, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual string + testFunc := func() { + actual = Pluralize(tc.s, ifOne, ifOther) + } + require.NotPanics(t, testFunc, "Pluralize(%d, %q, %q)", len(tc.s), ifOne, ifOther) + assert.Equal(t, tc.exp, actual, "result of Pluralize(%d, %q, %q)", len(tc.s), ifOne, ifOther) + }) + } +} + +func TestPluralEnding(t *testing.T) { + tests := []struct { + len int + exp string + }{ + {len: 0, exp: "s"}, + {len: 1, exp: ""}, + {len: 2, exp: "s"}, + {len: 3, exp: "s"}, + {len: 5, exp: "s"}, + {len: 50, exp: "s"}, + } + + for _, tc := range tests { + t.Run(fmt.Sprintf("%d", tc.len), func(t *testing.T) { + vals := make([]bool, tc.len) + var actual string + testFunc := func() { + actual = PluralEnding(vals) + } + require.NotPanics(t, testFunc, "PluralEnding(%d)", tc.len) + assert.Equal(t, tc.exp, actual, "result of PluralEnding(%d)", tc.len) + }) + } +} diff --git a/proto/cosmos/quarantine/v1beta1/genesis.proto b/proto/cosmos/quarantine/v1beta1/genesis.proto index 2fb2581a7e..a900efa296 100644 --- a/proto/cosmos/quarantine/v1beta1/genesis.proto +++ b/proto/cosmos/quarantine/v1beta1/genesis.proto @@ -3,7 +3,6 @@ package cosmos.quarantine.v1beta1; import "cosmos/quarantine/v1beta1/quarantine.proto"; import "cosmos_proto/cosmos.proto"; -import "gogoproto/gogo.proto"; option go_package = "github.com/provenance-io/provenance/x/quarantine"; diff --git a/proto/cosmos/quarantine/v1beta1/tx.proto b/proto/cosmos/quarantine/v1beta1/tx.proto index 5579edff68..2262c46ca2 100644 --- a/proto/cosmos/quarantine/v1beta1/tx.proto +++ b/proto/cosmos/quarantine/v1beta1/tx.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package cosmos.quarantine.v1beta1; import "amino/amino.proto"; -import "cosmos/base/query/v1beta1/pagination.proto"; import "cosmos/base/v1beta1/coin.proto"; import "cosmos/msg/v1/msg.proto"; import "cosmos/quarantine/v1beta1/quarantine.proto"; diff --git a/proto/provenance/metadata/v1/scope.proto b/proto/provenance/metadata/v1/scope.proto index a09fd255ec..1c198e0dec 100644 --- a/proto/provenance/metadata/v1/scope.proto +++ b/proto/provenance/metadata/v1/scope.proto @@ -81,8 +81,20 @@ message Scope { repeated Party owners = 3 [(gogoproto.nullable) = false]; // Addresses in this list are authorized to receive off-chain data associated with this scope. repeated string data_access = 4; - // An address that controls the value associated with this scope. Standard blockchain accounts and marker accounts - // are supported for this value. This attribute may only be changed by the entity indicated once it is set. + // The address that controls the value associated with this scope. + // + // The value owner is actually tracked by the bank module using a coin with the denom "nft/". + // The value owner can be changed using WriteScope or anything that transfers funds, e.g. MsgSend. + // + // During WriteScope: + // - If this field is empty, it indicates that there should not be a change to the value owner. + // I.e. Once a scope has a value owner, it will always have one (until it's deleted). + // - If this field has a value, the existing value owner will be looked up, and + // - If there's already an existing value owner, they must be a signer, + // and the coin will be transferred to the new value owner. + // - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + // If the scope already exists, the owners must be signers (just like changing other fields). + // If it's a new scope, there's no special signer limitations related to the value owner. string value_owner_address = 5; // Whether all parties in this scope and its sessions must be present in this scope's owners field. // This also enables use of optional=true scope owners and session parties. diff --git a/testutil/events_builder.go b/testutil/events_builder.go new file mode 100644 index 0000000000..882ec32764 --- /dev/null +++ b/testutil/events_builder.go @@ -0,0 +1,187 @@ +package testutil + +import ( + "testing" + + "github.com/stretchr/testify/require" + + abci "github.com/cometbft/cometbft/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/gogoproto/proto" +) + +// EventsBuilder helps put together a list of events for testing purposes. +// Create one using NewEventsBuilder, then add events using the methods like AddEvent or AddSendCoins. +// Once everything has been added, use Build() to get the final list of events. +type EventsBuilder struct { + Events sdk.Events + T *testing.T +} + +// NewEventsBuilder creates a new EventsBuilder that will use the provided T for error checking. +// If a T is provided, errors will cause test failures, otherwise errors will cause panics. +func NewEventsBuilder(t *testing.T) *EventsBuilder { + return &EventsBuilder{T: t} +} + +// Build returns the list of Events built so far. +func (b *EventsBuilder) Build() sdk.Events { + return b.Events +} + +// AddEvent adds one or more sdk.Event entries to this builder. +func (b *EventsBuilder) AddEvent(events ...sdk.Event) *EventsBuilder { + b.Events = append(b.Events, events...) + return b +} + +// AddEvents adds all of the provided sdk.Events to this builder. +func (b *EventsBuilder) AddEvents(events sdk.Events) *EventsBuilder { + b.Events = append(b.Events, events...) + return b +} + +// AddTypedEvent converts each of the provided events into an sdk.Event then adds it to this builder. +func (b *EventsBuilder) AddTypedEvent(tevs ...proto.Message) *EventsBuilder { + if b.T != nil { + b.T.Helper() + } + var err error + events := make(sdk.Events, len(tevs)) + for i, tev := range tevs { + events[i], err = sdk.TypedEventToEvent(tev) + switch { + case b.T != nil: + require.NoError(b.T, err, "TypedEventToEvent(%#v)", tev) + case err != nil: + panic(err) + } + } + return b.AddEvents(events) +} + +// AddSendCoins adds the events emitted during SendCoins. +func (b *EventsBuilder) AddSendCoins(from, to sdk.AccAddress, amount sdk.Coins) *EventsBuilder { + return b.AddEvents(SendCoinsEvents(from, to, amount)) +} + +// AddSendCoinsStrs adds the events emitted during SendCoins, but takes in the values as strings. +func (b *EventsBuilder) AddSendCoinsStrs(from, to, amount string) *EventsBuilder { + return b.AddEvents(SendCoinsEventsStrs(from, to, amount)) +} + +// SendCoinsEvents creates the events emitted during SendCoins. +func SendCoinsEvents(from, to sdk.AccAddress, amount sdk.Coins) sdk.Events { + return SendCoinsEventsStrs(from.String(), to.String(), amount.String()) +} + +// SendCoinsEventsStrs creates the events emitted during SendCoins, but takes in the values as strings. +func SendCoinsEventsStrs(from, to, amount string) sdk.Events { + return sdk.Events{ + {Type: "coin_spent", Attributes: []abci.EventAttribute{ + {Key: "spender", Value: from}, + {Key: "amount", Value: amount}, + }}, + {Type: "coin_received", Attributes: []abci.EventAttribute{ + {Key: "receiver", Value: to}, + {Key: "amount", Value: amount}, + }}, + {Type: "transfer", Attributes: []abci.EventAttribute{ + {Key: "recipient", Value: to}, + {Key: "sender", Value: from}, + {Key: "amount", Value: amount}, + }}, + {Type: "message", Attributes: []abci.EventAttribute{ + {Key: "sender", Value: from}, + }}, + } +} + +// AddFailedSendCoins adds the events emitted during SendCoins when there's an error from the send restrictions. +func (b *EventsBuilder) AddFailedSendCoins(from sdk.AccAddress, amount sdk.Coins) *EventsBuilder { + return b.AddEvents(FailedSendCoinsEvents(from, amount)) +} + +// AddFailedSendCoinsStrs adds the events emitted during SendCoins when there's +// an error from the send restrictions, but takes in the values as strings. +func (b *EventsBuilder) AddFailedSendCoinsStrs(from, amount string) *EventsBuilder { + return b.AddEvents(FailedSendCoinsEventsStrs(from, amount)) +} + +// FailedSendCoinsEvents creates the events emitted during SendCoins when there's an error from the send restrictions. +func FailedSendCoinsEvents(from sdk.AccAddress, amount sdk.Coins) sdk.Events { + return FailedSendCoinsEventsStrs(from.String(), amount.String()) +} + +// FailedSendCoinsEventsStrs creates the events emitted during SendCoins when there's +// an error from the send restrictions, but takes in the values as strings. +func FailedSendCoinsEventsStrs(from, amount string) sdk.Events { + return sdk.Events{{Type: "coin_spent", Attributes: []abci.EventAttribute{ + {Key: "spender", Value: from}, + {Key: "amount", Value: amount}, + }}} +} + +// AddMintCoins adds the events emitted during MintCoins. +func (b *EventsBuilder) AddMintCoins(moduleName string, amount sdk.Coins) *EventsBuilder { + return b.AddEvents(MintCoinsEvents(moduleName, amount)) +} + +// AddMintCoinsStrs adds the events emitted during MintCoins, but takes in the values as strings. +// Note that the first argument should be the bech32 address string of the module account (not the module name). +func (b *EventsBuilder) AddMintCoinsStrs(moduleAddr, amount string) *EventsBuilder { + return b.AddEvents(MintCoinsEventsStrs(moduleAddr, amount)) +} + +// MintCoinsEvents creates the events emitted during MintCoins. +func MintCoinsEvents(moduleName string, amount sdk.Coins) sdk.Events { + return MintCoinsEventsStrs(authtypes.NewModuleAddress(moduleName).String(), amount.String()) +} + +// MintCoinsEventsStrs creates the events emitted during MintCoins, but takes in the values as strings. +// Note that the first argument should be the bech32 address string of the module account (not the module name). +func MintCoinsEventsStrs(moduleAddr, amount string) sdk.Events { + return sdk.Events{ + {Type: "coin_received", Attributes: []abci.EventAttribute{ + {Key: "receiver", Value: moduleAddr}, + {Key: "amount", Value: amount}, + }}, + {Type: "coinbase", Attributes: []abci.EventAttribute{ + {Key: "minter", Value: moduleAddr}, + {Key: "amount", Value: amount}, + }}, + } +} + +// AddBurnCoins adds the events emitted during BurnCoins. +func (b *EventsBuilder) AddBurnCoins(moduleName string, amount sdk.Coins) *EventsBuilder { + return b.AddEvents(BurnCoinsEvents(moduleName, amount)) +} + +// AddBurnCoinsStrs adds the events emitted during BurnCoins, but takes in the values as strings. +// Note that the first argument should be the bech32 address string of the module account (not the module name). +func (b *EventsBuilder) AddBurnCoinsStrs(moduleAddr, amount string) *EventsBuilder { + return b.AddEvents(BurnCoinsEventsStrs(moduleAddr, amount)) +} + +// BurnCoinsEvents creates the events emitted during BurnCoins. +func BurnCoinsEvents(moduleName string, amount sdk.Coins) sdk.Events { + return BurnCoinsEventsStrs(authtypes.NewModuleAddress(moduleName).String(), amount.String()) +} + +// BurnCoinsEventsStrs creates the events emitted during BurnCoins, but takes in the values as strings. +// Note that the first argument should be the bech32 address string of the module account (not the module name). +func BurnCoinsEventsStrs(moduleAddr, amount string) sdk.Events { + return sdk.Events{ + {Type: "coin_spent", Attributes: []abci.EventAttribute{ + {Key: "spender", Value: moduleAddr}, + {Key: "amount", Value: amount}, + }}, + {Type: "burn", Attributes: []abci.EventAttribute{ + {Key: "burner", Value: moduleAddr}, + {Key: "amount", Value: amount}, + }}, + } +} diff --git a/testutil/testlog/logs.go b/testutil/testlog/logs.go new file mode 100644 index 0000000000..ee5e7ba277 --- /dev/null +++ b/testutil/testlog/logs.go @@ -0,0 +1,138 @@ +package testlog + +import ( + "fmt" + "strconv" + "strings" + "testing" +) + +// WriteSlice writes the contents of the provided slice to the test logs. +// +// The first line will have a header with the name and length. +// Then, there'll be one line for each entry, each with the format "[] = " (with the = lined up). +func WriteSlice[S ~[]E, E any](t testing.TB, name string, vals S) { + t.Helper() + t.Log(createSliceLogString(name, vals)) +} + +// WriteVariables writes the provided named variables to the test logs under the given header. +// +// The namesAndValues args must be provided in pairs, the first is the name, the second is the value. +// Name args must be a string. Value args can be anything, and do not need to all be the same type. +// +// The test fails immediately if an odd number of namesAndValues are provided or if any name args are not a string. +// +// E.g. WriteVariables(t, "addresses", "addr1", addr1, "addr2", addr2) +// +// The first line will contain the provided header and a count of variables being logged. +// Then, there'll be one line for each variable, each with the format " = " (with the = lined up). +// +// See also: WriteSlice. +func WriteVariables(t testing.TB, header string, namesAndValues ...interface{}) { + t.Helper() + t.Log(newNamedValues(t, namesAndValues).GetLogString(header)) +} + +// WriteVariable writes the provided named variable to the test logs in the format " = ". +func WriteVariable(t testing.TB, name string, value interface{}) { + t.Helper() + t.Logf("%s = %s", name, valueString(value)) +} + +// createSliceLogString creates a multi-line string of the given slice for the purposes of logging. +func createSliceLogString[S ~[]E, E any](name string, vals S) string { + return namedValuesForSlice(name, vals).GetLogString(name) +} + +// namedValue associates a name with a value. +type namedValue struct { + Name string + Value interface{} +} + +// newNamedValue creates a new namedValue. +func newNamedValue(name string, value interface{}) *namedValue { + return &namedValue{Name: name, Value: value} +} + +// namedValues is a slice of namedValue entries. +type namedValues []*namedValue + +// newNamedValues creates a namedValues from the provided namesAndValues. +// +// The namesAndValues args must be provided in pairs, the first is the name, the second is the value. +// Name args must be a string. Value args can be anything, and do not need to all be the same type. +// +// The test fails immediately if an odd number of namesAndValues are provided or if any name args are not a string. +// +// E.g. newNamedValues(t, "addr1", addr1, "addr2", addr2) +func newNamedValues(t testing.TB, namesAndValues []interface{}) namedValues { + t.Helper() + if len(namesAndValues) == 0 { + return nil + } + if len(namesAndValues)%2 != 0 { + t.Fatalf("Odd number of name/value args provided.\n%s", + createSliceLogString("namesAndValues", namesAndValues)) + } + rv := make(namedValues, 0, len(namesAndValues)/2) + for i := 0; i < len(namesAndValues); i += 2 { + nameArg := namesAndValues[i] + valueArg := namesAndValues[i+1] + name, nameOK := nameArg.(string) + if !nameOK { + t.Fatalf("Invalid name/value arg pair: name arg has type %T, expected string.\n"+ + " (name)=args[%d]=%#v\n"+ + "(value)=args[%d]=%#v\n"+ + "%s", + nameArg, i, nameArg, i+1, valueArg, createSliceLogString("namesAndValues", namesAndValues)) + } + rv = append(rv, newNamedValue(name, valueArg)) + } + return rv +} + +// namedValuesForSlice creates a namedValues representing the provided slice and its values. +func namedValuesForSlice[S ~[]E, E any](name string, vals S) namedValues { + rv := make(namedValues, len(vals)) + for i, val := range vals { + rv[i] = newNamedValue(fmt.Sprintf("%s[%d]", name, i), val) + } + return rv +} + +// GetLogString creates a multi-line string with the provided header and each of the entries in this namedValues. +func (s namedValues) GetLogString(header string) string { + nameWidth := 0 + for _, entry := range s { + if len(entry.Name) > nameWidth { + nameWidth = len(entry.Name) + } + } + lineFmt := "%" + strconv.Itoa(nameWidth) + "s = %s" + + lines := make([]string, len(s)) + for i, entry := range s { + if entry == nil { + lines[i] = "" + } else { + lines[i] = fmt.Sprintf(lineFmt, entry.Name, valueString(entry.Value)) + } + } + return fmt.Sprintf("%s (%d):\n%s", header, len(s), strings.Join(lines, "\n")) +} + +// valueString creates a string of the given value. +func valueString(value interface{}) string { + if value == nil { + return "" + } + switch val := value.(type) { + case string: + return val + case fmt.Stringer: + return val.String() + } + return fmt.Sprintf("%#v", value) +} diff --git a/x/exchange/expected_keepers.go b/x/exchange/expected_keepers.go index 223c6baa77..82fdfef34e 100644 --- a/x/exchange/expected_keepers.go +++ b/x/exchange/expected_keepers.go @@ -8,6 +8,7 @@ import ( attrtypes "github.com/provenance-io/provenance/x/attribute/types" markertypes "github.com/provenance-io/provenance/x/marker/types" + metadatatypes "github.com/provenance-io/provenance/x/metadata/types" ) type AccountKeeper interface { @@ -39,3 +40,8 @@ type MarkerKeeper interface { AddSetNetAssetValues(ctx sdk.Context, marker markertypes.MarkerAccountI, netAssetValues []markertypes.NetAssetValue, source string) error GetNetAssetValue(ctx sdk.Context, markerDenom, priceDenom string) (*markertypes.NetAssetValue, error) } + +type MetadataKeeper interface { + AddSetNetAssetValues(ctx sdk.Context, scopeID metadatatypes.MetadataAddress, netAssetValues []metadatatypes.NetAssetValue, source string) error + GetNetAssetValue(ctx sdk.Context, metadataDenom, priceDenom string) (*metadatatypes.NetAssetValue, error) +} diff --git a/x/exchange/keeper/commitments.go b/x/exchange/keeper/commitments.go index 82cf038dcf..79cda4dcb4 100644 --- a/x/exchange/keeper/commitments.go +++ b/x/exchange/keeper/commitments.go @@ -264,14 +264,14 @@ func (k Keeper) ValidateAndCollectCommitmentCreationFee(ctx sdk.Context, marketI return nil } -// lookupNav gets a nav from the provided known navs, or if not known, gets it from the marker module. -func (k Keeper) lookupNav(ctx sdk.Context, markerDenom, priceDenom string, known []exchange.NetAssetPrice) *exchange.NetAssetPrice { +// lookupNav gets a nav from the provided known navs, or if not known, gets it from the marker or metadata module. +func (k Keeper) lookupNav(ctx sdk.Context, assetsDenom, priceDenom string, known []exchange.NetAssetPrice) *exchange.NetAssetPrice { for _, nav := range known { - if nav.Assets.Denom == markerDenom && nav.Price.Denom == priceDenom { + if nav.Assets.Denom == assetsDenom && nav.Price.Denom == priceDenom { return &nav } } - return k.GetNav(ctx, markerDenom, priceDenom) + return k.GetNav(ctx, assetsDenom, priceDenom) } // CalculateCommitmentSettlementFee calculates the fee that the exchange must be paid (by the market) for the provided @@ -396,7 +396,7 @@ func (k Keeper) SettleCommitments(ctx sdk.Context, req *exchange.MsgMarketCommit } // Do the transfers - xFerCtx := markertypes.WithTransferAgent(ctx, admin) + xFerCtx := markertypes.WithTransferAgents(ctx, admin) var xferErrs []error for _, transfer := range transfers { err = k.DoTransfer(xFerCtx, transfer.Inputs, transfer.Outputs) diff --git a/x/exchange/keeper/commitments_test.go b/x/exchange/keeper/commitments_test.go index ac6aa11068..0d7ae81889 100644 --- a/x/exchange/keeper/commitments_test.go +++ b/x/exchange/keeper/commitments_test.go @@ -12,6 +12,7 @@ import ( "github.com/provenance-io/provenance/x/exchange" "github.com/provenance-io/provenance/x/exchange/keeper" markertypes "github.com/provenance-io/provenance/x/marker/types" + metadatatypes "github.com/provenance-io/provenance/x/metadata/types" ) func (s *TestSuite) TestKeeper_GetCommitmentAmount() { @@ -1811,15 +1812,19 @@ func (s *TestSuite) TestKeeper_SettleCommitments() { navSource := func(marketID uint32) string { return fmt.Sprintf("x/exchange market %d", marketID) } + scopeID1 := s.scopeID("1_scope") + scopeID2 := s.scopeID("2_scope") tests := []struct { name string setup func() + mdKeeper *MockMetadataKeeper markerKeeper *MockMarkerKeeper holdKeeper *MockHoldKeeper bankKeeper *MockBankKeeper req *exchange.MsgMarketCommitmentSettleRequest expEvents sdk.Events + expMDCalls MetadataCalls expMarkerCalls MarkerCalls expHoldCalls HoldCalls expBankCalls BankCalls @@ -1926,6 +1931,10 @@ func (s *TestSuite) TestKeeper_SettleCommitments() { {Assets: s.coin("11apple"), Price: s.coin("700nhash")}, {Assets: s.coin("12banana"), Price: s.coin("62cherry")}, {Assets: s.coin("13banana"), Price: s.coin("1500nhash")}, + {Assets: scopeID1.Coin(), Price: s.coin("71cherry")}, + {Assets: scopeID1.Coin(), Price: s.coin("400nhash")}, + {Assets: scopeID2.Coin(), Price: s.coin("5cherry")}, + {Assets: scopeID2.Coin(), Price: s.coin("6600nhash")}, }, EventTag: "testtag3", }, @@ -1933,6 +1942,26 @@ func (s *TestSuite) TestKeeper_SettleCommitments() { s.untypeEvent(exchange.NewEventCommitmentReleased(s.addr3.String(), 4, s.coins("10apple,10banana"), "testtag3")), s.untypeEvent(exchange.NewEventFundsCommitted(s.addr5.String(), 4, s.coins("10apple,10banana"), "testtag3")), }, + expMDCalls: MetadataCalls{ + AddSetNetAssetValues: []*MDAddSetNetAssetValuesArgs{ + { + ScopeID: scopeID1, + NAVs: []metadatatypes.NetAssetValue{ + {Price: s.coin("71cherry"), Volume: 1}, + {Price: s.coin("400nhash"), Volume: 1}, + }, + Source: navSource(4), + }, + { + ScopeID: scopeID2, + NAVs: []metadatatypes.NetAssetValue{ + {Price: s.coin("5cherry"), Volume: 1}, + {Price: s.coin("6600nhash"), Volume: 1}, + }, + Source: navSource(4), + }, + }, + }, expMarkerCalls: MarkerCalls{ GetMarker: []sdk.AccAddress{appleMarker.GetAddress(), bananaMarker.GetAddress()}, AddSetNetAssetValues: []*AddSetNetAssetValuesArgs{ @@ -2147,6 +2176,9 @@ func (s *TestSuite) TestKeeper_SettleCommitments() { tc.setup() } + if tc.mdKeeper == nil { + tc.mdKeeper = NewMockMetadataKeeper() + } if tc.markerKeeper == nil { tc.markerKeeper = NewMockMarkerKeeper() } @@ -2169,6 +2201,7 @@ func (s *TestSuite) TestKeeper_SettleCommitments() { } kpr := s.k. + WithMetadataKeeper(tc.mdKeeper). WithMarkerKeeper(tc.markerKeeper). WithBankKeeper(tc.bankKeeper). WithHoldKeeper(tc.holdKeeper) @@ -2183,6 +2216,7 @@ func (s *TestSuite) TestKeeper_SettleCommitments() { actEvents := em.Events() s.assertEqualEvents(tc.expEvents, actEvents, "events emitted during SettleCommitments") + s.assertMetadataKeeperCalls(tc.mdKeeper, tc.expMDCalls, "SettleCommitments") s.assertMarkerKeeperCalls(tc.markerKeeper, tc.expMarkerCalls, "SettleCommitments") s.assertBankKeeperCalls(tc.bankKeeper, tc.expBankCalls, "SettleCommitments") s.assertHoldKeeperCalls(tc.holdKeeper, tc.expHoldCalls, "SettleCommitments") diff --git a/x/exchange/keeper/export_test.go b/x/exchange/keeper/export_test.go index c68b8227ec..978f6982ca 100644 --- a/x/exchange/keeper/export_test.go +++ b/x/exchange/keeper/export_test.go @@ -42,6 +42,12 @@ func (k Keeper) WithMarkerKeeper(markerKeeper exchange.MarkerKeeper) Keeper { return k } +// WithMetadataKeeper is a test-only method that returns a new Keeper that uses the provided MetadataKeeper. +func (k Keeper) WithMetadataKeeper(metadataKeeper exchange.MetadataKeeper) Keeper { + k.metadataKeeper = metadataKeeper + return k +} + // GetStore is a test-only exposure of getStore. func (k Keeper) GetStore(ctx sdk.Context) storetypes.KVStore { return k.getStore(ctx) diff --git a/x/exchange/keeper/fulfillment.go b/x/exchange/keeper/fulfillment.go index e94b38ba2e..af72520a44 100644 --- a/x/exchange/keeper/fulfillment.go +++ b/x/exchange/keeper/fulfillment.go @@ -3,6 +3,7 @@ package keeper import ( "errors" "fmt" + "strings" sdkmath "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" @@ -13,6 +14,7 @@ import ( "github.com/provenance-io/provenance/x/exchange" markertypes "github.com/provenance-io/provenance/x/marker/types" + metadatatypes "github.com/provenance-io/provenance/x/metadata/types" ) // sumAssetsAndPrice gets the sum of assets, and the sum of prices of the provided orders. @@ -257,7 +259,7 @@ func (k Keeper) SettleOrders(ctx sdk.Context, req *exchange.MsgMarketSettleReque return errors.New("settlement unexpectedly resulted in all orders fully filled") } - return k.closeSettlement(markertypes.WithTransferAgent(ctx, admin), store, req.MarketId, settlement) + return k.closeSettlement(markertypes.WithTransferAgents(ctx, admin), store, req.MarketId, settlement) } // closeSettlement does all the processing needed to complete a settlement. @@ -330,50 +332,86 @@ func (k Keeper) closeSettlement(ctx sdk.Context, store storetypes.KVStore, marke func (k Keeper) recordNAVs(ctx sdk.Context, marketID uint32, navs []exchange.NetAssetPrice) { source := fmt.Sprintf("x/exchange market %d", marketID) - // convert them to what the marker module needs. + // convert them to what the marker and/or metadata modules need. + var markerDenoms, metadataDenoms []string markerNAVs := make(map[string][]markertypes.NetAssetValue) - var denomOrder []string + metadataNAVs := make(map[string][]metadatatypes.NetAssetValue) for _, nav := range navs { + isMetadataDenom := strings.HasPrefix(nav.Assets.Denom, metadatatypes.DenomPrefix) + if !nav.Assets.Amount.IsUint64() { k.logErrorf(ctx, "could not record net-asset-value of %q at a price of %q: asset volume greater than max uint64", nav.Assets, nav.Price) - k.emitEvent(ctx, &markertypes.EventSetNetAssetValue{ - Denom: nav.Assets.Denom, - Price: nav.Price.String(), - Volume: nav.Assets.Amount.String(), - Source: source, - }) + if isMetadataDenom { + k.emitEvent(ctx, &metadatatypes.EventSetNetAssetValue{ + ScopeId: strings.TrimPrefix(nav.Assets.Denom, metadatatypes.DenomPrefix), + Price: nav.Price.String(), + Volume: nav.Assets.Amount.String(), + Source: source, + }) + } else { + k.emitEvent(ctx, &markertypes.EventSetNetAssetValue{ + Denom: nav.Assets.Denom, + Price: nav.Price.String(), + Volume: nav.Assets.Amount.String(), + Source: source, + }) + } continue } - if _, known := markerNAVs[nav.Assets.Denom]; !known { - denomOrder = append(denomOrder, nav.Assets.Denom) + if isMetadataDenom { + if _, known := metadataNAVs[nav.Assets.Denom]; !known { + metadataDenoms = append(metadataDenoms, nav.Assets.Denom) + } + metadataNAV := metadatatypes.NetAssetValue{ + Price: nav.Price, + Volume: nav.Assets.Amount.Uint64(), + } + metadataNAVs[nav.Assets.Denom] = append(metadataNAVs[nav.Assets.Denom], metadataNAV) + } else { + if _, known := markerNAVs[nav.Assets.Denom]; !known { + markerDenoms = append(markerDenoms, nav.Assets.Denom) + } + markerNAV := markertypes.NetAssetValue{ + Price: nav.Price, + Volume: nav.Assets.Amount.Uint64(), + } + markerNAVs[nav.Assets.Denom] = append(markerNAVs[nav.Assets.Denom], markerNAV) } + } - markerNAV := markertypes.NetAssetValue{ - Price: nav.Price, - Volume: nav.Assets.Amount.Uint64(), + // Record the metadata NAVs. + for _, denom := range metadataDenoms { + scopeID, err := metadatatypes.MetadataAddressFromDenom(denom) + if err != nil { + k.logErrorf(ctx, "error getting metadata address: %v", err) + k.emitMetadataNAVEvents(ctx, scopeID, metadataNAVs[denom], source) + continue + } + err = k.metadataKeeper.AddSetNetAssetValues(ctx, scopeID, metadataNAVs[denom], source) + if err != nil { + k.logErrorf(ctx, "error setting net-asset-values for %q: %v", denom, err) } - markerNAVs[nav.Assets.Denom] = append(markerNAVs[nav.Assets.Denom], markerNAV) } - // Get the markers and record the NAVs. - for _, denom := range denomOrder { + // Record the marker NAVs. + for _, denom := range markerDenoms { markerAddr, err := markertypes.MarkerAddress(denom) if err != nil { k.logErrorf(ctx, "error creating marker address for asset denom %q: %v", denom, err) - k.emitNAVEvents(ctx, denom, markerNAVs[denom], source) + k.emitMarkerNAVEvents(ctx, denom, markerNAVs[denom], source) continue } marker, err := k.markerKeeper.GetMarker(ctx, markerAddr) if err != nil { k.logErrorf(ctx, "error getting asset marker %q: %v", denom, err) - k.emitNAVEvents(ctx, denom, markerNAVs[denom], source) + k.emitMarkerNAVEvents(ctx, denom, markerNAVs[denom], source) continue } if marker == nil { k.logInfof(ctx, "no marker found for asset denom %q", denom) - k.emitNAVEvents(ctx, denom, markerNAVs[denom], source) + k.emitMarkerNAVEvents(ctx, denom, markerNAVs[denom], source) continue } @@ -384,9 +422,9 @@ func (k Keeper) recordNAVs(ctx sdk.Context, marketID uint32, navs []exchange.Net } } -// emitNAVEvents emits the marker module's EventSetNetAssetValue events for the given navs. +// emitMarkerNAVEvents emits the marker module's EventSetNetAssetValue events for the given navs. // The AddSetNetAssetValues func does this too, so this should only be used when that isn't being called. -func (k Keeper) emitNAVEvents(ctx sdk.Context, denom string, navs []markertypes.NetAssetValue, source string) { +func (k Keeper) emitMarkerNAVEvents(ctx sdk.Context, denom string, navs []markertypes.NetAssetValue, source string) { events := make([]proto.Message, len(navs)) for i, nav := range navs { events[i] = markertypes.NewEventSetNetAssetValue(denom, nav.Price, nav.Volume, source) @@ -394,8 +432,31 @@ func (k Keeper) emitNAVEvents(ctx sdk.Context, denom string, navs []markertypes. k.emitEvents(ctx, events) } -// GetNav looks up a NAV from the marker module and returns it as a NetAssetPrice. +// emitMetadataNAVEvents emits the metadata module's EventSetNetAssetValue events for the given navs. +// The AddSetNetAssetValues func does this too, so this should only be used when that isn't being called. +func (k Keeper) emitMetadataNAVEvents(ctx sdk.Context, scopeID metadatatypes.MetadataAddress, navs []metadatatypes.NetAssetValue, source string) { + events := make([]proto.Message, len(navs)) + for i, nav := range navs { + events[i] = metadatatypes.NewEventSetNetAssetValue(scopeID, nav.Price, nav.Volume, source) + } + k.emitEvents(ctx, events) +} + +// GetNav looks up a NAV from the marker or metadata module and returns it as a NetAssetPrice. func (k Keeper) GetNav(ctx sdk.Context, assetsDenom, priceDenom string) *exchange.NetAssetPrice { + if strings.HasPrefix(assetsDenom, metadatatypes.DenomPrefix) { + // Get the nav from the metadata module. + nav, _ := k.metadataKeeper.GetNetAssetValue(ctx, assetsDenom, priceDenom) + if nav == nil { + return nil + } + return &exchange.NetAssetPrice{ + Assets: sdk.Coin{Denom: assetsDenom, Amount: sdkmath.NewIntFromUint64(nav.Volume)}, + Price: nav.Price, + } + } + + // Look for the nav in the marker module. nav, _ := k.markerKeeper.GetNetAssetValue(ctx, assetsDenom, priceDenom) if nav == nil { return nil diff --git a/x/exchange/keeper/fulfillment_test.go b/x/exchange/keeper/fulfillment_test.go index e204a5dc01..9e768677f0 100644 --- a/x/exchange/keeper/fulfillment_test.go +++ b/x/exchange/keeper/fulfillment_test.go @@ -7,6 +7,7 @@ import ( "github.com/provenance-io/provenance/x/exchange" markertypes "github.com/provenance-io/provenance/x/marker/types" + metadatatypes "github.com/provenance-io/provenance/x/metadata/types" ) func (s *TestSuite) TestKeeper_FillBids() { @@ -532,7 +533,7 @@ func (s *TestSuite) TestKeeper_FillBids() { expEvents: []*exchange.EventOrderFilled{ {OrderId: 13, Assets: "12apple", Price: "60plum", MarketId: 6}, }, - adlEvents: sdk.Events{s.navSetEvent("12apple", "60plum", 6)}, + adlEvents: sdk.Events{s.markerNavSetEvent("12apple", "60plum", 6)}, expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("60plum")}}}, expBankCalls: BankCalls{ BlockedAddr: []sdk.AccAddress{s.addr2, s.addr5}, @@ -563,7 +564,7 @@ func (s *TestSuite) TestKeeper_FillBids() { expEvents: []*exchange.EventOrderFilled{ {OrderId: 13, Assets: "12apple", Price: "60plum", MarketId: 6}, }, - adlEvents: sdk.Events{s.navSetEvent("12apple", "60plum", 6)}, + adlEvents: sdk.Events{s.markerNavSetEvent("12apple", "60plum", 6)}, expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("60plum")}}}, expBankCalls: BankCalls{ BlockedAddr: []sdk.AccAddress{s.addr2, s.addr5}, @@ -595,7 +596,7 @@ func (s *TestSuite) TestKeeper_FillBids() { expEvents: []*exchange.EventOrderFilled{ {OrderId: 13, Assets: "184467440737095516150apple", Price: "60plum", MarketId: 6}, }, - adlEvents: sdk.Events{s.navSetEvent("184467440737095516150apple", "60plum", 6)}, + adlEvents: sdk.Events{s.markerNavSetEvent("184467440737095516150apple", "60plum", 6)}, expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("60plum")}}}, expBankCalls: BankCalls{ BlockedAddr: []sdk.AccAddress{s.addr2, s.addr5}, @@ -1395,7 +1396,7 @@ func (s *TestSuite) TestKeeper_FillAsks() { expEvents: []*exchange.EventOrderFilled{ {OrderId: 13, Assets: "12apple", Price: "60plum", MarketId: 6}, }, - adlEvents: sdk.Events{s.navSetEvent("12apple", "60plum", 6)}, + adlEvents: sdk.Events{s.markerNavSetEvent("12apple", "60plum", 6)}, expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("12apple")}}}, expBankCalls: BankCalls{ BlockedAddr: []sdk.AccAddress{s.addr5, s.addr2}, @@ -1426,7 +1427,7 @@ func (s *TestSuite) TestKeeper_FillAsks() { expEvents: []*exchange.EventOrderFilled{ {OrderId: 13, Assets: "12apple", Price: "60plum", MarketId: 6}, }, - adlEvents: sdk.Events{s.navSetEvent("12apple", "60plum", 6)}, + adlEvents: sdk.Events{s.markerNavSetEvent("12apple", "60plum", 6)}, expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("12apple")}}}, expBankCalls: BankCalls{ BlockedAddr: []sdk.AccAddress{s.addr5, s.addr2}, @@ -1457,7 +1458,7 @@ func (s *TestSuite) TestKeeper_FillAsks() { expEvents: []*exchange.EventOrderFilled{ {OrderId: 13, Assets: "184467440737095516150apple", Price: "60plum", MarketId: 6}, }, - adlEvents: sdk.Events{s.navSetEvent("184467440737095516150apple", "60plum", 6)}, + adlEvents: sdk.Events{s.markerNavSetEvent("184467440737095516150apple", "60plum", 6)}, expHoldCalls: HoldCalls{ReleaseHold: []*ReleaseHoldArgs{{addr: s.addr2, funds: s.coins("184467440737095516150apple")}}}, expBankCalls: BankCalls{ BlockedAddr: []sdk.AccAddress{s.addr5, s.addr2}, @@ -1730,12 +1731,14 @@ func (s *TestSuite) TestKeeper_FillAsks() { func (s *TestSuite) TestKeeper_SettleOrders() { appleMarker := s.markerAccount("1000000000apple") + scopeID1 := s.scopeID("1_scopeID1") tests := []struct { name string bankKeeper *MockBankKeeper holdKeeper *MockHoldKeeper markerKeeper *MockMarkerKeeper + mdKeeper *MockMetadataKeeper setup func() marketID uint32 askOrderIDs []uint64 @@ -1748,6 +1751,7 @@ func (s *TestSuite) TestKeeper_SettleOrders() { expHoldCalls HoldCalls expBankCalls BankCalls expMarkerCalls MarkerCalls + expMDCalls MetadataCalls expLog []string }{ // Tests on error conditions. @@ -2033,6 +2037,49 @@ func (s *TestSuite) TestKeeper_SettleOrders() { }, }, }, + { + name: "one ask one bid: scope", + setup: func() { + s.requireCreateMarket(exchange.Market{MarketId: 1}) + store := s.getStore() + s.requireSetOrderInStore(store, exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + Assets: scopeID1.Coin(), Price: s.coin("5peach"), MarketId: 1, Seller: s.addr3.String(), + })) + s.requireSetOrderInStore(store, exchange.NewOrder(5).WithBid(&exchange.BidOrder{ + Assets: scopeID1.Coin(), Price: s.coin("5peach"), MarketId: 1, Buyer: s.addr4.String(), + })) + }, + marketID: 1, + askOrderIDs: []uint64{1}, + bidOrderIDs: []uint64{5}, + expectPartial: false, + expEvents: []proto.Message{ + &exchange.EventOrderFilled{OrderId: 1, Assets: scopeID1.Coin().String(), Price: "5peach", MarketId: 1}, + &exchange.EventOrderFilled{OrderId: 5, Assets: scopeID1.Coin().String(), Price: "5peach", MarketId: 1}, + }, + expHoldCalls: HoldCalls{ + ReleaseHold: []*ReleaseHoldArgs{ + {addr: s.addr3, funds: scopeID1.Coins()}, + {addr: s.addr4, funds: s.coins("5peach")}, + }, + }, + expBankCalls: BankCalls{ + BlockedAddr: []sdk.AccAddress{s.addr4, s.addr3}, + SendCoins: []*SendCoinsArgs{ + {ctxHasQuarantineBypass: true, fromAddr: s.addr3, toAddr: s.addr4, amt: scopeID1.Coins()}, + {ctxHasQuarantineBypass: true, fromAddr: s.addr4, toAddr: s.addr3, amt: s.coins("5peach")}, + }, + }, + expMDCalls: MetadataCalls{ + AddSetNetAssetValues: []*MDAddSetNetAssetValuesArgs{ + { + ScopeID: scopeID1, + NAVs: []metadatatypes.NetAssetValue{{Price: s.coin("5peach"), Volume: 1}}, + Source: "x/exchange market 1", + }, + }, + }, + }, { name: "one ask one bid: both full, no fees, error getting marker", markerKeeper: NewMockMarkerKeeper().WithGetMarkerErr(appleMarker.GetAddress(), "sample apple error"), @@ -2054,7 +2101,7 @@ func (s *TestSuite) TestKeeper_SettleOrders() { &exchange.EventOrderFilled{OrderId: 1, Assets: "1apple", Price: "5peach", MarketId: 1}, &exchange.EventOrderFilled{OrderId: 5, Assets: "1apple", Price: "5peach", MarketId: 1}, }, - adlEvents: sdk.Events{s.navSetEvent("1apple", "5peach", 1)}, + adlEvents: sdk.Events{s.markerNavSetEvent("1apple", "5peach", 1)}, expHoldCalls: HoldCalls{ ReleaseHold: []*ReleaseHoldArgs{ {addr: s.addr3, funds: s.coins("1apple")}, @@ -2093,7 +2140,7 @@ func (s *TestSuite) TestKeeper_SettleOrders() { &exchange.EventOrderFilled{OrderId: 1, Assets: "1apple", Price: "5peach", MarketId: 1}, &exchange.EventOrderFilled{OrderId: 5, Assets: "1apple", Price: "5peach", MarketId: 1}, }, - adlEvents: sdk.Events{s.navSetEvent("1apple", "5peach", 1)}, + adlEvents: sdk.Events{s.markerNavSetEvent("1apple", "5peach", 1)}, expHoldCalls: HoldCalls{ ReleaseHold: []*ReleaseHoldArgs{ {addr: s.addr3, funds: s.coins("1apple")}, @@ -2139,7 +2186,7 @@ func (s *TestSuite) TestKeeper_SettleOrders() { {addr: s.addr4, funds: s.coins("5peach")}, }, }, - adlEvents: sdk.Events{s.navSetEvent("184467440737095516150apple", "5peach", 1)}, + adlEvents: sdk.Events{s.markerNavSetEvent("184467440737095516150apple", "5peach", 1)}, expBankCalls: BankCalls{ BlockedAddr: []sdk.AccAddress{s.addr4, s.addr3}, SendCoins: []*SendCoinsArgs{ @@ -2492,6 +2539,9 @@ func (s *TestSuite) TestKeeper_SettleOrders() { if tc.markerKeeper == nil { tc.markerKeeper = NewMockMarkerKeeper() } + if tc.mdKeeper == nil { + tc.mdKeeper = NewMockMetadataKeeper() + } expEvents := untypeEvents(s, tc.expEvents) if len(tc.adlEvents) > 0 { @@ -2521,7 +2571,8 @@ func (s *TestSuite) TestKeeper_SettleOrders() { kpr := s.k.WithAccountKeeper(s.accKeeper). WithBankKeeper(tc.bankKeeper). WithHoldKeeper(tc.holdKeeper). - WithMarkerKeeper(tc.markerKeeper) + WithMarkerKeeper(tc.markerKeeper). + WithMetadataKeeper(tc.mdKeeper) s.logBuffer.Reset() var err error testFunc := func() { @@ -2534,6 +2585,7 @@ func (s *TestSuite) TestKeeper_SettleOrders() { s.assertHoldKeeperCalls(tc.holdKeeper, tc.expHoldCalls, "SettleOrders") s.assertBankKeeperCalls(tc.bankKeeper, tc.expBankCalls, "SettleOrders") s.assertMarkerKeeperCalls(tc.markerKeeper, tc.expMarkerCalls, "SettleOrders") + s.assertMetadataKeeperCalls(tc.mdKeeper, tc.expMDCalls, "SettleOrders") outputLog := s.getLogOutput("SettleOrders") actLog := s.splitOutputLog(outputLog) @@ -2567,28 +2619,34 @@ func (s *TestSuite) TestKeeper_SettleOrders() { } func (s *TestSuite) TestKeeper_GetNav() { + scopeDenom := s.scopeID("scope_uuid").Denom() tests := []struct { - name string - markerKeeper *MockMarkerKeeper - assetsDenom string - priceDenom string - expNav *exchange.NetAssetPrice + name string + metadataKeeper *MockMetadataKeeper + markerKeeper *MockMarkerKeeper + assetsDenom string + priceDenom string + expNav *exchange.NetAssetPrice + expMetadataCall bool + expMarkerCall bool }{ { - name: "error getting nav", - markerKeeper: NewMockMarkerKeeper().WithGetNetAssetValueError("apple", "pear", "injected test error"), - assetsDenom: "apple", - priceDenom: "pear", - expNav: nil, + name: "marker: error getting nav", + markerKeeper: NewMockMarkerKeeper().WithGetNetAssetValueError("apple", "pear", "injected test error"), + assetsDenom: "apple", + priceDenom: "pear", + expNav: nil, + expMarkerCall: true, }, { - name: "no nav found", - assetsDenom: "apple", - priceDenom: "pear", - expNav: nil, + name: "marker: no nav found", + assetsDenom: "apple", + priceDenom: "pear", + expNav: nil, + expMarkerCall: true, }, { - name: "nav exists", + name: "marker: nav exists", markerKeeper: NewMockMarkerKeeper(). WithGetNetAssetValueResult(sdk.NewInt64Coin("apple", 500), sdk.NewInt64Coin("pear", 12)), assetsDenom: "apple", @@ -2597,16 +2655,55 @@ func (s *TestSuite) TestKeeper_GetNav() { Assets: sdk.NewInt64Coin("apple", 500), Price: sdk.NewInt64Coin("pear", 12), }, + expMarkerCall: true, + }, + { + name: "metadata: error getting nav", + metadataKeeper: NewMockMetadataKeeper().WithGetNetAssetValueErrors("injected test problem"), + assetsDenom: scopeDenom, + priceDenom: "pear", + expNav: nil, + expMetadataCall: true, + }, + { + name: "metadata: no nav found", + assetsDenom: scopeDenom, + priceDenom: "pear", + expNav: nil, + expMetadataCall: true, + }, + { + name: "metadata: nav exists", + metadataKeeper: NewMockMetadataKeeper().WithGetNetAssetValueResult(sdk.NewInt64Coin("pear", 53)), + assetsDenom: scopeDenom, + priceDenom: "pear", + expNav: &exchange.NetAssetPrice{ + Assets: sdk.NewInt64Coin(scopeDenom, 1), + Price: sdk.NewInt64Coin("pear", 53), + }, + expMetadataCall: true, }, } for _, tc := range tests { s.Run(tc.name, func() { + var expMetadataCalls MetadataCalls + if tc.expMetadataCall { + expMetadataCalls.WithGetNetAssetValue(tc.assetsDenom, tc.priceDenom) + } + var expMarkerCalls MarkerCalls + if tc.expMarkerCall { + expMarkerCalls.WithGetNetAssetValue(tc.assetsDenom, tc.priceDenom) + } + + if tc.metadataKeeper == nil { + tc.metadataKeeper = NewMockMetadataKeeper() + } if tc.markerKeeper == nil { tc.markerKeeper = NewMockMarkerKeeper() } - kpr := s.k.WithMarkerKeeper(tc.markerKeeper) + kpr := s.k.WithMarkerKeeper(tc.markerKeeper).WithMetadataKeeper(tc.metadataKeeper) var actNav *exchange.NetAssetPrice testFunc := func() { actNav = kpr.GetNav(s.ctx, tc.assetsDenom, tc.priceDenom) @@ -2616,6 +2713,8 @@ func (s *TestSuite) TestKeeper_GetNav() { s.Assert().Equal(tc.expNav.Assets.String(), actNav.Assets.String(), "assets (string)") s.Assert().Equal(tc.expNav.Price.String(), actNav.Price.String(), "price (string)") } + s.assertMetadataKeeperCalls(tc.metadataKeeper, expMetadataCalls, "GetNav(%q, %q)", tc.assetsDenom, tc.priceDenom) + s.assertMarkerKeeperCalls(tc.markerKeeper, expMarkerCalls, "GetNav(%q, %q)", tc.assetsDenom, tc.priceDenom) }) } } diff --git a/x/exchange/keeper/keeper.go b/x/exchange/keeper/keeper.go index 9f005423a0..891692e5ac 100644 --- a/x/exchange/keeper/keeper.go +++ b/x/exchange/keeper/keeper.go @@ -32,11 +32,12 @@ type Keeper struct { cdc codec.BinaryCodec storeKey storetypes.StoreKey - accountKeeper exchange.AccountKeeper - attrKeeper exchange.AttributeKeeper - bankKeeper exchange.BankKeeper - holdKeeper exchange.HoldKeeper - markerKeeper exchange.MarkerKeeper + accountKeeper exchange.AccountKeeper + attrKeeper exchange.AttributeKeeper + bankKeeper exchange.BankKeeper + holdKeeper exchange.HoldKeeper + markerKeeper exchange.MarkerKeeper + metadataKeeper exchange.MetadataKeeper authority string feeCollectorName string @@ -45,6 +46,7 @@ type Keeper struct { func NewKeeper(cdc codec.BinaryCodec, storeKey storetypes.StoreKey, feeCollectorName string, accountKeeper exchange.AccountKeeper, attrKeeper exchange.AttributeKeeper, bankKeeper exchange.BankKeeper, holdKeeper exchange.HoldKeeper, markerKeeper exchange.MarkerKeeper, + metadataKeeper exchange.MetadataKeeper, ) Keeper { rv := Keeper{ cdc: cdc, @@ -54,6 +56,7 @@ func NewKeeper(cdc codec.BinaryCodec, storeKey storetypes.StoreKey, feeCollector bankKeeper: bankKeeper, holdKeeper: holdKeeper, markerKeeper: markerKeeper, + metadataKeeper: metadataKeeper, authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), feeCollectorName: feeCollectorName, } diff --git a/x/exchange/keeper/market.go b/x/exchange/keeper/market.go index fe3df8a69b..bd8fe5844b 100644 --- a/x/exchange/keeper/market.go +++ b/x/exchange/keeper/market.go @@ -1548,7 +1548,7 @@ func (k Keeper) WithdrawMarketFunds(ctx sdk.Context, marketID uint32, toAddr sdk return fmt.Errorf("%s is not allowed to receive funds", toAddr) } marketAddr := exchange.GetMarketAddress(marketID) - xferCtx := markertypes.WithTransferAgent(ctx, admin) + xferCtx := markertypes.WithTransferAgents(ctx, admin) if toAddr.Equals(admin) { xferCtx = quarantine.WithBypass(xferCtx) } diff --git a/x/exchange/keeper/mocks_test.go b/x/exchange/keeper/mocks_test.go index a65112737e..463576d029 100644 --- a/x/exchange/keeper/mocks_test.go +++ b/x/exchange/keeper/mocks_test.go @@ -4,15 +4,18 @@ import ( "context" "errors" "fmt" + "strings" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/provenance-io/provenance/internal/provutils" attrtypes "github.com/provenance-io/provenance/x/attribute/types" "github.com/provenance-io/provenance/x/exchange" markertypes "github.com/provenance-io/provenance/x/marker/types" + metadatatypes "github.com/provenance-io/provenance/x/metadata/types" "github.com/provenance-io/provenance/x/quarantine" ) @@ -297,7 +300,7 @@ func NewMockBankKeeper() *MockBankKeeper { } // WithSendCoinsResults queues up the provided error strings to be returned from SendCoins. -// An empty string means no error. Each entry is used only once. If entries run out, nil is returned. +// An empty string means no result or error. Each entry is used only once. If entries run out, nil is returned. // This method both updates the receiver and returns it. func (k *MockBankKeeper) WithSendCoinsResults(errs ...string) *MockBankKeeper { k.SendCoinsResultsQueue = append(k.SendCoinsResultsQueue, errs...) @@ -415,13 +418,17 @@ func (s *TestSuite) assertBankKeeperCalls(mk *MockBankKeeper, expected BankCalls // NewSendCoinsArgs creates a new record of args provided to a call to SendCoins. func NewSendCoinsArgs(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) *SendCoinsArgs { - return &SendCoinsArgs{ + rv := &SendCoinsArgs{ ctxHasQuarantineBypass: quarantine.HasBypass(ctx), - ctxTransferAgent: markertypes.GetTransferAgent(ctx), fromAddr: fromAddr, toAddr: toAddr, amt: amt, } + xferAgents := markertypes.GetTransferAgents(ctx) + if len(xferAgents) > 0 { + rv.ctxTransferAgent = xferAgents[0] + } + return rv } // sendCoinsArgsString creates a string of a SendCoinsArgs @@ -433,13 +440,17 @@ func (s *TestSuite) sendCoinsArgsString(a *SendCoinsArgs) string { // NewSendCoinsFromAccountToModuleArgs creates a new record of args provided to a call to SendCoinsFromAccountToModule. func NewSendCoinsFromAccountToModuleArgs(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) *SendCoinsFromAccountToModuleArgs { - return &SendCoinsFromAccountToModuleArgs{ + rv := &SendCoinsFromAccountToModuleArgs{ ctxHasQuarantineBypass: quarantine.HasBypass(ctx), - ctxTransferAgent: markertypes.GetTransferAgent(ctx), senderAddr: senderAddr, recipientModule: recipientModule, amt: amt, } + xferAgents := markertypes.GetTransferAgents(ctx) + if len(xferAgents) > 0 { + rv.ctxTransferAgent = xferAgents[0] + } + return rv } // sendCoinsFromAccountToModuleArgsString creates a string of a SendCoinsFromAccountToModuleArgs @@ -451,12 +462,16 @@ func (s *TestSuite) sendCoinsFromAccountToModuleArgsString(a *SendCoinsFromAccou // NewInputOutputCoinsArgs creates a new record of args provided to a call to InputOutputCoins. func NewInputOutputCoinsArgs(ctx context.Context, inputs []banktypes.Input, outputs []banktypes.Output) *InputOutputCoinsArgs { - return &InputOutputCoinsArgs{ + rv := &InputOutputCoinsArgs{ ctxHasQuarantineBypass: quarantine.HasBypass(ctx), - ctxTransferAgent: markertypes.GetTransferAgent(ctx), inputs: inputs, outputs: outputs, } + xferAgents := markertypes.GetTransferAgents(ctx) + if len(xferAgents) > 0 { + rv.ctxTransferAgent = xferAgents[0] + } + return rv } // inputOutputCoinsArgsString creates a string of a InputOutputCoinsArgs substituting the address names as possible. @@ -818,10 +833,7 @@ func (k *MockMarkerKeeper) AddSetNetAssetValues(_ sdk.Context, marker markertype } func (k *MockMarkerKeeper) GetNetAssetValue(_ sdk.Context, markerDenom, priceDenom string) (*markertypes.NetAssetValue, error) { - k.Calls.GetNetAssetValue = append(k.Calls.GetNetAssetValue, &GetNetAssetValueArgs{ - markerDenom: markerDenom, - priceDenom: priceDenom, - }) + k.Calls.WithGetNetAssetValue(markerDenom, priceDenom) var nav *markertypes.NetAssetValue var err error if k.GetNetAssetValueMap != nil && k.GetNetAssetValueMap[markerDenom] != nil && k.GetNetAssetValueMap[markerDenom][priceDenom] != nil { @@ -842,14 +854,14 @@ func (s *TestSuite) assertGetMarkerCalls(mk *MockMarkerKeeper, expected []sdk.Ac func (s *TestSuite) assertAddSetNetAssetValuesCalls(mk *MockMarkerKeeper, expected []*AddSetNetAssetValuesArgs, msg string, args ...interface{}) bool { s.T().Helper() return assertEqualSlice(s, expected, mk.Calls.AddSetNetAssetValues, s.getAddSetNetAssetValuesArgsDenom, - msg+" AddSetNetAssetValues calls", args...) + msg+" marker AddSetNetAssetValues calls", args...) } // assertGetNetAssetValueCalls asserts that a mock keeper's Calls.GetNetAssetValueArgs match the provided expected calls. func (s *TestSuite) assertGetNetAssetValueCalls(mk *MockMarkerKeeper, expected []*GetNetAssetValueArgs, msg string, args ...interface{}) bool { s.T().Helper() return assertEqualSlice(s, expected, mk.Calls.GetNetAssetValue, s.getNetAssetValueArgsString, - msg+" GetNetAssetValue calls", args...) + msg+" marker GetNetAssetValue calls", args...) } // assertMarkerKeeperCalls asserts that all the calls made to a mock marker keeper match the provided expected calls. @@ -860,6 +872,14 @@ func (s *TestSuite) assertMarkerKeeperCalls(mk *MockMarkerKeeper, expected Marke return s.assertGetNetAssetValueCalls(mk, expected.GetNetAssetValue, msg, args...) && rv } +// WithGetNetAssetValue adds the provided args to the GetNetAssetValue list. +func (c *MarkerCalls) WithGetNetAssetValue(markerDenom, priceDenom string) { + c.GetNetAssetValue = append(c.GetNetAssetValue, &GetNetAssetValueArgs{ + markerDenom: markerDenom, + priceDenom: priceDenom, + }) +} + // NewAddSetNetAssetValuesArgs creates a new record of args provided to a call to AddSetNetAssetValues. func NewAddSetNetAssetValuesArgs(marker markertypes.MarkerAccountI, netAssetValues []markertypes.NetAssetValue, source string) *AddSetNetAssetValuesArgs { return &AddSetNetAssetValuesArgs{ @@ -892,3 +912,190 @@ func (s *TestSuite) getNetAssetValueArgsString(args *GetNetAssetValueArgs) strin } return fmt.Sprintf("%q->%q", md, pd) } + +// ############################################################################# +// ########################### ############################ +// ######################### MockMetadataKeeper ########################## +// ########################### ############################ +// ############################################################################# + +var _ exchange.MetadataKeeper = (*MockMetadataKeeper)(nil) + +// MockMetadataKeeper satisfies the exchange.MetadataKeeper interface but just records the calls and allows dictation of results. +type MockMetadataKeeper struct { + Calls MetadataCalls + AddSetNetAssetValuesResultsQueue []string + GetNetAssetValueResultsQueue []*MDGetNetAssetValueResult +} + +// MetadataCalls contains all the calls that the mock metadata keeper makes. +type MetadataCalls struct { + AddSetNetAssetValues []*MDAddSetNetAssetValuesArgs + GetNetAssetValue []*MDGetNetAssetValueArgs +} + +// MDAddSetNetAssetValuesArgs is a record of a call that is made to AddSetNetAssetValues (in the metadata module). +type MDAddSetNetAssetValuesArgs struct { + ScopeID metadatatypes.MetadataAddress + NAVs []metadatatypes.NetAssetValue + Source string +} + +type MDGetNetAssetValueResult provutils.Pair[*metadatatypes.NetAssetValue, string] + +// MDGetNetAssetValueArgs is a record of a call that is made to GetNetAssetValue (in the metadata module). +type MDGetNetAssetValueArgs provutils.Pair[string, string] + +// NewMockMetadataKeeper creates a new empty MockMetadataKeeper. +// Follow it up with WithAddSetNetAssetValuesErrors, WithGetNetAssetValueErrors, +// and/or WithGetNetAssetValueResults to dictate results. +func NewMockMetadataKeeper() *MockMetadataKeeper { + return &MockMetadataKeeper{} +} + +// WithAddSetNetAssetValuesErrors queues up the provided error strings to be returned from AddSetNetAssetValues. +// An empty string means no error. Each entry is used only once. If entries run out, nil is returned. +// This method both updates the receiver and returns it. +func (k *MockMetadataKeeper) WithAddSetNetAssetValuesErrors(errs ...string) *MockMetadataKeeper { + k.AddSetNetAssetValuesResultsQueue = append(k.AddSetNetAssetValuesResultsQueue, errs...) + return k +} + +// WithGetNetAssetValueErrors queues up the provided error strings to be returned from GetNetAssetValue. +// An empty string means no error. Each entry is used only once. If entries run out, nil is returned. +// This method both updates the receiver and returns it. +// See also: WithGetNetAssetValueResults. +func (k *MockMetadataKeeper) WithGetNetAssetValueErrors(errs ...string) *MockMetadataKeeper { + for _, err := range errs { + k.GetNetAssetValueResultsQueue = append(k.GetNetAssetValueResultsQueue, NewMDGetNetAssetValueResult(nil, err)) + } + return k +} + +func (k *MockMetadataKeeper) WithGetNetAssetValueResult(price sdk.Coin) *MockMetadataKeeper { + k.GetNetAssetValueResultsQueue = append(k.GetNetAssetValueResultsQueue, + NewMDGetNetAssetValueResult(&metadatatypes.NetAssetValue{Price: price, Volume: 1}, "")) + return k +} + +func (k *MockMetadataKeeper) AddSetNetAssetValues(_ sdk.Context, scopeID metadatatypes.MetadataAddress, navs []metadatatypes.NetAssetValue, source string) error { + k.Calls.WithAddSetNetAssetValues(scopeID, navs, source) + if len(k.AddSetNetAssetValuesResultsQueue) > 0 { + rv := k.AddSetNetAssetValuesResultsQueue[0] + k.AddSetNetAssetValuesResultsQueue = k.AddSetNetAssetValuesResultsQueue[1:] + if len(rv) > 0 { + return errors.New(rv) + } + } + return nil +} + +func (k *MockMetadataKeeper) GetNetAssetValue(_ sdk.Context, metadataDenom, priceDenom string) (*metadatatypes.NetAssetValue, error) { + k.Calls.WithGetNetAssetValue(metadataDenom, priceDenom) + if len(k.GetNetAssetValueResultsQueue) > 0 { + rv := k.GetNetAssetValueResultsQueue[0] + k.GetNetAssetValueResultsQueue = k.GetNetAssetValueResultsQueue[1:] + return rv.Nav(), rv.Err() + } + return nil, nil +} + +// assertMDAddSetNetAssetValues asserts that a mock keeper's Calls.AddSetNetAssetValues match the provided expected calls. +func (s *TestSuite) assertMDAddSetNetAssetValues(mk *MockMetadataKeeper, expected []*MDAddSetNetAssetValuesArgs, msg string, args ...interface{}) bool { + s.T().Helper() + return assertEqualSlice(s, expected, mk.Calls.AddSetNetAssetValues, mdAddNAVArgsString, + msg+" metadata AddSetNetAssetValues calls", args...) +} + +// assertMDGetNetAssetValueCalls asserts that a mock keeper's Calls.GetNetAssetValue match the provided expected calls. +func (s *TestSuite) assertMDGetNetAssetValueCalls(mk *MockMetadataKeeper, expected []*MDGetNetAssetValueArgs, msg string, args ...interface{}) bool { + s.T().Helper() + return assertEqualSlice(s, expected, mk.Calls.GetNetAssetValue, mdNAVString, + msg+" metadata GetNetAssetValue calls", args...) +} + +// assertMetadataKeeperCalls asserts that all the calls made to a mock metadata keeper match the provided expected calls. +func (s *TestSuite) assertMetadataKeeperCalls(mk *MockMetadataKeeper, expected MetadataCalls, msg string, args ...interface{}) bool { + s.T().Helper() + rv := s.assertMDAddSetNetAssetValues(mk, expected.AddSetNetAssetValues, msg, args...) + return s.assertMDGetNetAssetValueCalls(mk, expected.GetNetAssetValue, msg, args...) && rv +} + +// WithAddSetNetAssetValues adds the provided args to the AddSetNetAssetValues list. +func (c *MetadataCalls) WithAddSetNetAssetValues(scopeID metadatatypes.MetadataAddress, navs []metadatatypes.NetAssetValue, source string) { + c.AddSetNetAssetValues = append(c.AddSetNetAssetValues, NewMDAddSetNetAssetValuesArgs(scopeID, navs, source)) +} + +// WithGetNetAssetValue adds the provided args to the GetNetAssetValue list. +func (c *MetadataCalls) WithGetNetAssetValue(metadataDenom, priceDenom string) { + c.GetNetAssetValue = append(c.GetNetAssetValue, NewMDGetNetAssetValueArgs(metadataDenom, priceDenom)) +} + +// NewMDAddSetNetAssetValuesArgs creates a new record of the args provided to a call to the metadata keeper's AddSetNetAssetValues method. +func NewMDAddSetNetAssetValuesArgs(scopeID metadatatypes.MetadataAddress, navs []metadatatypes.NetAssetValue, source string) *MDAddSetNetAssetValuesArgs { + return &MDAddSetNetAssetValuesArgs{ + ScopeID: scopeID, + NAVs: navs, + Source: source, + } +} + +// String returns a string of this MDAddSetNetAssetValuesArgs. +func (a MDAddSetNetAssetValuesArgs) String() string { + navStrs := sliceStrings(a.NAVs, func(nav metadatatypes.NetAssetValue) string { + return nav.String() + }) + return fmt.Sprintf("%s(%s):%s", + a.ScopeID.String(), + a.Source, + strings.Join(navStrs, ","), + ) +} + +// mdAddNAVArgsString is the same as MDAddSetNetAssetValuesArgs.String but with a pointer arg. +func mdAddNAVArgsString(p *MDAddSetNetAssetValuesArgs) string { + return p.String() +} + +// NewMDGetNetAssetValueArgs creates a new record of the args provided to a call to the metadata keeper's GetNetAssetValue method. +func NewMDGetNetAssetValueArgs(metadataDenom, priceDenom string) *MDGetNetAssetValueArgs { + return (*MDGetNetAssetValueArgs)(provutils.NewPair(metadataDenom, priceDenom)) +} + +// MetadataDenom gets the metadataDenom string associated with the GetNetAssetValue method. +func (p MDGetNetAssetValueArgs) MetadataDenom() string { + return p.A +} + +// PriceDenom gets the priceDenom string associated with the GetNetAssetValue method. +func (p MDGetNetAssetValueArgs) PriceDenom() string { + return p.B +} + +// String returns a string of this MDGetNetAssetValueArgs. +func (p MDGetNetAssetValueArgs) String() string { + return fmt.Sprintf("%s:%s", p.MetadataDenom(), p.PriceDenom()) +} + +// mdNAVString is the same as MDGetNetAssetValueArgs.String but with a pointer arg. +func mdNAVString(p *MDGetNetAssetValueArgs) string { + return p.String() +} + +// NewMDGetNetAssetValueResult creates a record of things associated with the GetNetAssetValue return values. +func NewMDGetNetAssetValueResult(nav *metadatatypes.NetAssetValue, err string) *MDGetNetAssetValueResult { + return (*MDGetNetAssetValueResult)(provutils.NewPair(nav, err)) +} + +// Nav returns the Net Asset Value arg of this MDGetNetAssetValueResult. +func (p MDGetNetAssetValueResult) Nav() *metadatatypes.NetAssetValue { + return p.A +} + +// Err returns the error arg of this MDGetNetAssetValueResult by converting it's string (B) into either an error or nil. +func (p MDGetNetAssetValueResult) Err() error { + if len(p.B) == 0 { + return nil + } + return errors.New(p.B) +} diff --git a/x/exchange/keeper/msg_server_test.go b/x/exchange/keeper/msg_server_test.go index f6b6315633..526f479211 100644 --- a/x/exchange/keeper/msg_server_test.go +++ b/x/exchange/keeper/msg_server_test.go @@ -1293,7 +1293,7 @@ func (s *TestSuite) TestMsgServer_FillBids() { s.untypeEvent(&exchange.EventOrderFilled{ OrderId: 54, Assets: "10apple", Price: "50pear", MarketId: 3, }), - s.navSetEvent("10apple", "50pear", 3), + s.markerNavSetEvent("10apple", "50pear", 3), }, }, { @@ -1351,7 +1351,7 @@ func (s *TestSuite) TestMsgServer_FillBids() { s.untypeEvent(&exchange.EventOrderFilled{ OrderId: 54, Assets: "10apple", Price: "50pear", MarketId: 3, }), - s.navSetEvent("10apple", "50pear", 3), + s.markerNavSetEvent("10apple", "50pear", 3), }, }, { @@ -1668,7 +1668,7 @@ func (s *TestSuite) TestMsgServer_FillBids() { }), // The net-asset-value event. - s.navSetEvent("13apple", "70pear", 1), + s.markerNavSetEvent("13apple", "70pear", 1), // Order creation fee events. s.eventCoinSpent(s.addr1, "10fig"), @@ -1780,7 +1780,7 @@ func (s *TestSuite) TestMsgServer_FillAsks() { s.untypeEvent(&exchange.EventOrderFilled{ OrderId: 54, Assets: "10apple", Price: "50pear", MarketId: 3, }), - s.navSetEvent("10apple", "50pear", 3), + s.markerNavSetEvent("10apple", "50pear", 3), }, }, { @@ -1838,7 +1838,7 @@ func (s *TestSuite) TestMsgServer_FillAsks() { s.untypeEvent(&exchange.EventOrderFilled{ OrderId: 54, Assets: "10apple", Price: "50pear", MarketId: 3, }), - s.navSetEvent("10apple", "50pear", 3), + s.markerNavSetEvent("10apple", "50pear", 3), }, }, { @@ -2154,7 +2154,7 @@ func (s *TestSuite) TestMsgServer_FillAsks() { }), // The net-asset-value event. - s.navSetEvent("13apple", "70pear", 1), + s.markerNavSetEvent("13apple", "70pear", 1), // Order creation fee events. s.eventCoinSpent(s.addr1, "10fig"), @@ -2434,7 +2434,7 @@ func (s *TestSuite) TestMsgServer_MarketSettle() { }), // The net-asset-value event (28). - s.navSetEvent("18apple", "185pear", 1), + s.markerNavSetEvent("18apple", "185pear", 1), }, }, { @@ -2608,7 +2608,7 @@ func (s *TestSuite) TestMsgServer_MarketSettle() { }), // The net-asset-value event (28). - s.navSetEvent("18apple", "185pear", 1), + s.markerNavSetEvent("18apple", "185pear", 1), }, }, { @@ -2725,7 +2725,7 @@ func (s *TestSuite) TestMsgServer_MarketSettle() { }), // The net-asset-value event. - s.navSetEvent("18apple", "185pear", 1), + s.markerNavSetEvent("18apple", "185pear", 1), }, }, { @@ -2801,7 +2801,7 @@ func (s *TestSuite) TestMsgServer_MarketSettle() { }), // The net-asset-value event. - s.navSetEvent("7apple", "75pear", 3), + s.markerNavSetEvent("7apple", "75pear", 3), }, }, { @@ -2877,7 +2877,7 @@ func (s *TestSuite) TestMsgServer_MarketSettle() { }), // The net-asset-value event. - s.navSetEvent("7apple", "70pear", 3), + s.markerNavSetEvent("7apple", "70pear", 3), }, }, { @@ -3085,7 +3085,7 @@ func (s *TestSuite) TestMsgServer_MarketSettle() { }), // The net-asset-value event. - s.navSetEvent("18apple", "185pear", 2), + s.markerNavSetEvent("18apple", "185pear", 2), }, }, } diff --git a/x/exchange/keeper/suite_test.go b/x/exchange/keeper/suite_test.go index f39c5d78ed..17a97440bc 100644 --- a/x/exchange/keeper/suite_test.go +++ b/x/exchange/keeper/suite_test.go @@ -7,10 +7,12 @@ import ( "strings" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/suite" sdkmath "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" + metadatatypes "github.com/provenance-io/provenance/x/metadata/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -1009,8 +1011,8 @@ func (s *TestSuite) markerAccount(supplyCoinStr string) markertypes.MarkerAccoun } } -// navSetEvent returns a new EventSetNetAssetValue converted to sdk.Event. -func (s *TestSuite) navSetEvent(assetsStr, priceStr string, marketID uint32) sdk.Event { +// markerNavSetEvent returns a new marke module EventSetNetAssetValue converted to sdk.Event. +func (s *TestSuite) markerNavSetEvent(assetsStr, priceStr string, marketID uint32) sdk.Event { assets := s.coin(assetsStr) event := &markertypes.EventSetNetAssetValue{ Denom: assets.Denom, @@ -1020,3 +1022,22 @@ func (s *TestSuite) navSetEvent(assetsStr, priceStr string, marketID uint32) sdk } return s.untypeEvent(event) } + +// metadataNavSetEvent returns a new metadata module EventSetNetAssetValue converted to sdk.Event. +func (s *TestSuite) metadataNavSetEvent(scopeID, priceStr string, marketID uint32) sdk.Event { + event := &metadatatypes.EventSetNetAssetValue{ + ScopeId: scopeID, + Price: priceStr, + Source: fmt.Sprintf("x/exchange market %d", marketID), + } + return s.untypeEvent(event) +} + +func (s *TestSuite) scopeID(base string) metadatatypes.MetadataAddress { + s.T().Helper() + s.Require().LessOrEqual(len(base), 16, "scopeID(%q): arg can only be 16 chars max") + bz := []byte(base + "________________")[:16] + uid, err := uuid.FromBytes(bz) + s.Require().NoError(err, "uuid.FromBytes(%q)", string(bz)) + return metadatatypes.ScopeMetadataAddress(uid) +} diff --git a/x/marker/keeper/keeper.go b/x/marker/keeper/keeper.go index b84332228e..3082d78221 100644 --- a/x/marker/keeper/keeper.go +++ b/x/marker/keeper/keeper.go @@ -427,3 +427,12 @@ func (k Keeper) GetReqAttrBypassAddrs() []sdk.AccAddress { func (k Keeper) IsReqAttrBypassAddr(addr sdk.AccAddress) bool { return k.reqAttrBypassAddrs.Has(addr) } + +// IsMarkerAccount returns true if the provided address is one for a marker account. +func (k Keeper) IsMarkerAccount(ctx sdk.Context, addr sdk.AccAddress) bool { + if len(addr) == 0 { + return false + } + store := ctx.KVStore(k.storeKey) + return store.Has(types.MarkerStoreKey(addr)) +} diff --git a/x/marker/keeper/keeper_test.go b/x/marker/keeper/keeper_test.go index afcb876d03..d4e35d73ac 100644 --- a/x/marker/keeper/keeper_test.go +++ b/x/marker/keeper/keeper_test.go @@ -460,7 +460,7 @@ func TestMintBurnCoins(t *testing.T) { require.Error(t, app.MarkerKeeper.DeleteMarker(ctx, user, "testcoin")) // Remove escrow balance from account - require.NoError(t, app.BankKeeper.SendCoinsFromAccountToModule(types.WithTransferAgent(ctx, user), addr, "mint", sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.OneInt()))), "sending coins to module") + require.NoError(t, app.BankKeeper.SendCoinsFromAccountToModule(types.WithTransferAgents(ctx, user), addr, "mint", sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.OneInt()))), "sending coins to module") // Succeeds because the bond denom coin was removed. require.NoError(t, app.MarkerKeeper.DeleteMarker(ctx, user, "testcoin")) @@ -2995,6 +2995,79 @@ func TestReqAttrBypassAddrs(t *testing.T) { } } +func TestIsMarkerAccount(t *testing.T) { + app := simapp.Setup(t) + ctx := app.BaseApp.NewContext(false) + + newMarker := func(denom string, status types.MarkerStatus, typ types.MarkerType) sdk.AccAddress { + addr, err := types.MarkerAddress(denom) + require.NoError(t, err, "MarkerAddress(%q)", denom) + marker := &types.MarkerAccount{ + BaseAccount: &authtypes.BaseAccount{Address: addr.String()}, + AccessControl: []types.AccessGrant{{ + Address: sdk.AccAddress("addr_with_perms_____").String(), + Permissions: types.AccessList{types.Access_Admin}, + }}, + Status: status, + Denom: denom, + Supply: sdkmath.NewInt(1000), + MarkerType: typ, + SupplyFixed: true, + AllowGovernanceControl: true, + } + + require.NotPanics(t, func() { + app.MarkerKeeper.SetNewMarker(ctx, marker) + }, "SetNewMarker %q", marker.Denom) + return addr + } + + normalAddr := sdk.AccAddress("normal_address______") + setNewAccount(app, ctx, &authtypes.BaseAccount{Address: normalAddr.String()}) + + tests := []struct { + name string + addr sdk.AccAddress + exp bool + }{ + {name: "nil address", addr: nil, exp: false}, + {name: "empty address", addr: nil, exp: false}, + {name: "unknown address", addr: sdk.AccAddress("unknown_address_____"), exp: false}, + {name: "normal address", addr: normalAddr, exp: false}, + { + name: "proposed restricted marker", + addr: newMarker("proposedrestricted", types.StatusProposed, types.MarkerType_RestrictedCoin), + exp: true, + }, + { + name: "active restricted marker", + addr: newMarker("activerestricted", types.StatusActive, types.MarkerType_RestrictedCoin), + exp: true, + }, + { + name: "proposed coin marker", + addr: newMarker("proposedcoin", types.StatusProposed, types.MarkerType_Coin), + exp: true, + }, + { + name: "active coin marker", + addr: newMarker("activecoin", types.StatusActive, types.MarkerType_Coin), + exp: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual bool + testFunc := func() { + actual = app.MarkerKeeper.IsMarkerAccount(ctx, tc.addr) + } + require.NotPanics(t, testFunc, "IsMarkerAccount") + assert.Equal(t, tc.exp, actual, "result from IsMarkerAccount") + }) + } +} + // dummyBankKeeper satisfies the types.BankKeeper interface but does nothing. type dummyBankKeeper struct{} diff --git a/x/marker/keeper/send_restrictions.go b/x/marker/keeper/send_restrictions.go index 27e799d81c..c361432d4c 100644 --- a/x/marker/keeper/send_restrictions.go +++ b/x/marker/keeper/send_restrictions.go @@ -37,7 +37,7 @@ func (k Keeper) SendRestrictionFn(goCtx context.Context, fromAddr, toAddr sdk.Ac } // If it's coming from a marker, make sure the withdraw is allowed. - admin := types.GetTransferAgent(ctx) + admins := types.GetTransferAgents(ctx) if fromMarker, _ := k.GetMarker(ctx, fromAddr); fromMarker != nil { // The only ways to legitimately send from a marker account is to have a transfer agent with // withdraw permissions, or through a feegrant. The only way to have a feegrant from @@ -47,13 +47,13 @@ func (k Keeper) SendRestrictionFn(goCtx context.Context, fromAddr, toAddr sdk.Ac // so we don't need to worry about its details in here, and that HasFeeGrantInUse is only ever // true when collecting fees. if !internalsdk.HasFeeGrantInUse(ctx) { - if len(admin) == 0 { + if len(admins) == 0 { return nil, fmt.Errorf("cannot withdraw from marker account %s (%s)", fromAddr.String(), fromMarker.GetDenom()) } - // That transfer agent must have withdraw access on the marker we're taking from. - if err := fromMarker.ValidateAddressHasAccess(admin, types.Access_Withdraw); err != nil { + // Need at least one admin that can make withdrawals. + if err := types.ValidateAtLeastOneAddrHasAccess(fromMarker, admins, types.Access_Withdraw); err != nil { return nil, err } } @@ -69,22 +69,24 @@ func (k Keeper) SendRestrictionFn(goCtx context.Context, fromAddr, toAddr sdk.Ac } } - // If it's going to a restricted marker, either the admin (if there is one) or + // If it's going to a restricted marker, either an admin (if there is one) or // fromAddr (if there isn't an admin) must have deposit access on that marker. toMarker, _ := k.GetMarker(ctx, toAddr) if toMarker != nil && toMarker.GetMarkerType() == types.MarkerType_RestrictedCoin { - addr := admin - if len(addr) == 0 { - addr = fromAddr - } - if err := toMarker.ValidateAddressHasAccess(addr, types.Access_Deposit); err != nil { - return nil, err + if len(admins) > 0 { + if err := types.ValidateAtLeastOneAddrHasAccess(toMarker, admins, types.Access_Deposit); err != nil { + return nil, err + } + } else { + if err := toMarker.ValidateAddressHasAccess(fromAddr, types.Access_Deposit); err != nil { + return nil, err + } } } // Check the ability to send each denom involved. for _, coin := range amt { - if err := k.validateSendDenom(ctx, fromAddr, toAddr, admin, coin.Denom, toMarker); err != nil { + if err := k.validateSendDenom(ctx, fromAddr, toAddr, admins, coin.Denom, toMarker); err != nil { return nil, err } } @@ -94,7 +96,7 @@ func (k Keeper) SendRestrictionFn(goCtx context.Context, fromAddr, toAddr sdk.Ac // validateSendDenom makes sure a send of the given denom is allowed for the given addresses. // This is NOT the validation that is needed for the marker Transfer endpoint. -func (k Keeper) validateSendDenom(ctx sdk.Context, fromAddr, toAddr, admin sdk.AccAddress, denom string, toMarker types.MarkerAccountI) error { +func (k Keeper) validateSendDenom(ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, admins []sdk.AccAddress, denom string, toMarker types.MarkerAccountI) error { markerAddr := types.MustGetMarkerAddress(denom) marker, err := k.GetMarker(ctx, markerAddr) if err != nil { @@ -117,7 +119,7 @@ func (k Keeper) validateSendDenom(ctx sdk.Context, fromAddr, toAddr, admin sdk.A } // If there's an admin that has transfer access, it's not a normal bank send and there's nothing more to do here. - if len(admin) > 0 && marker.AddressHasAccess(admin, types.Access_Transfer) { + if len(admins) > 0 && types.AtLeastOneAddrHasAccess(marker, admins, types.Access_Transfer) { return nil } @@ -139,12 +141,17 @@ func (k Keeper) validateSendDenom(ctx sdk.Context, fromAddr, toAddr, admin sdk.A // intermediary account and deposit them from there, or give the bypass account deposit and transfer permissions. // It's assumed that a marker address cannot be in the bypass list. if toMarker != nil { - addr := admin - if len(addr) == 0 { - addr = fromAddr + if len(admins) == 0 { + return fmt.Errorf("%s does not have %s on %s marker (%s)", + fromAddr, types.Access_Transfer, denom, marker.GetAddress()) } - return fmt.Errorf("%s does not have %s on %s marker (%s)", - addr, types.Access_Transfer, denom, marker.GetAddress()) + addrs := make([]string, 1+len(admins)) + addrs[0] = fromAddr.String() + for i, admin := range admins { + addrs[i+1] = admin.String() + } + return fmt.Errorf("none of %q have %s on %s marker (%s)", + addrs, types.Access_Transfer, denom, marker.GetAddress()) } // If there aren't any required attributes, transfer permission is required unless coming from a bypass account. @@ -155,11 +162,7 @@ func (k Keeper) validateSendDenom(ctx sdk.Context, fromAddr, toAddr, admin sdk.A if k.IsReqAttrBypassAddr(fromAddr) { return nil } - addr := admin - if len(addr) == 0 { - addr = fromAddr - } - return fmt.Errorf("%s does not have transfer permissions for %s", addr.String(), denom) + return fmt.Errorf("%s does not have transfer permissions for %s", fromAddr.String(), denom) } // At this point, we know there are required attributes and that fromAddr does not have transfer permission. diff --git a/x/marker/keeper/send_restrictions_test.go b/x/marker/keeper/send_restrictions_test.go index b63fe4fbf7..b7b1736d03 100644 --- a/x/marker/keeper/send_restrictions_test.go +++ b/x/marker/keeper/send_restrictions_test.go @@ -36,12 +36,12 @@ func TestSendRestrictionFn(t *testing.T) { ctxP := func(ctx sdk.Context) *sdk.Context { return &ctx } - owner := sdk.AccAddress("owner_address_______") + owner := sdk.AccAddress("owner_address_______") // cosmos1damkuetjtaskgerjv4ehxh6lta047h6l54ft8x app.AccountKeeper.SetAccount(ctx, app.AccountKeeper.NewAccountWithAddress(ctx, owner)) require.NoError(t, app.NameKeeper.SetNameRecord(ctx, "kyc.provenance.io", owner, false), "SetNameRecord kyc.provenance.io") require.NoError(t, app.NameKeeper.SetNameRecord(ctx, "not-kyc.provenance.io", owner, false), "SetNameRecord not-kyc.provenance.io") - addrWithAttrs := sdk.AccAddress("addr_with_attributes") + addrWithAttrs := sdk.AccAddress("addr_with_attributes") // cosmos1v9jxgujlwa5hg6zlv968gunfvf6hgetn0pdywn addrWithAttrsStr := addrWithAttrs.String() require.NoError(t, app.AttributeKeeper.SetAttribute(ctx, attrTypes.Attribute{ @@ -62,19 +62,20 @@ func TestSendRestrictionFn(t *testing.T) { owner, ), "SetAttribute not-kyc.provenance.io") - addrWithoutAttrs := sdk.AccAddress("addr_without_attribs") - addrWithTransfer := sdk.AccAddress("addr_with_transfer__") - addrWithForceTransfer := sdk.AccAddress("addr_with_force_tran") - addrWithDeposit := sdk.AccAddress("addrWithDeposit_____") - addrWithWithdraw := sdk.AccAddress("addrWithWithdraw____") - addrWithTranDep := sdk.AccAddress("addrWithTranDep_____") - addrWithTranWithdraw := sdk.AccAddress("addrWithTranWithdraw") - addrWithTranDepWithdraw := sdk.AccAddress("addrWithTranDepWithd") - addrWithDepWithdraw := sdk.AccAddress("addrWithDepWithdraw_") - addrWithDenySend := sdk.AccAddress("addrWithDenySend_____") - addrOther := sdk.AccAddress("addrOther___________") - - addrFeeCollector := app.MarkerKeeper.GetFeeCollectorAddr() + addrWithoutAttrs := sdk.AccAddress("addr_without_attribs") // cosmos1v9jxgujlwa5hg6r0w4697ct5w3exjcnnex0ue3 + addrWithTransfer := sdk.AccAddress("addr_with_transfer__") // cosmos1v9jxgujlwa5hg6zlw3exzmnnvejhyh6l0estr5 + addrWithForceTransfer := sdk.AccAddress("addr_with_force_tran") // cosmos1v9jxgujlwa5hg6zlvehhycm9ta68yctwk3cx3n + addrWithDeposit := sdk.AccAddress("addrWithDeposit_____") // cosmos1v9jxgujhd96xs3r9wphhx6t5ta047h6lkfv2e0 + addrWithWithdraw := sdk.AccAddress("addrWithWithdraw____") // cosmos1v9jxgujhd96xs4mfw35xgunpwa047h6lf5yvu2 + addrWithTranDep := sdk.AccAddress("addrWithTranDep_____") // cosmos1v9jxgujhd96xs4rjv9hygetsta047h6lvx7ley + addrWithTranWithdraw := sdk.AccAddress("addrWithTranWithdraw") // cosmos1v9jxgujhd96xs4rjv9h9w6t5dpj8ycth4fy3dc + addrWithTranDepWithdraw := sdk.AccAddress("addrWithTranDepWithd") // cosmos1v9jxgujhd96xs4rjv9hygets2a5hg6ry5lx6jm + addrWithDepWithdraw := sdk.AccAddress("addrWithDepWithdraw_") // cosmos1v9jxgujhd96xs3r9wptkjargv3exza6lsjke8w + addrWithDenySend := sdk.AccAddress("addrWithDenySend____") // cosmos1v9jxgujhd96xs3r9deu4xetwv3047h6lttsvaa + addrOther := sdk.AccAddress("addrOther___________") // cosmos1v9jxguj0w35x2ujlta047h6lta047h6ldtkks6 + addrOther2 := sdk.AccAddress("addrOther2__________") // cosmos1v9jxguj0w35x2u3jta047h6lta047h6lucvw6t + + addrFeeCollector := app.MarkerKeeper.GetFeeCollectorAddr() // cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta bypassAddrs := app.MarkerKeeper.GetReqAttrBypassAddrs() var addrWithBypass, addrWithBypassNoDep sdk.AccAddress for _, addr := range bypassAddrs { @@ -147,11 +148,11 @@ func TestSendRestrictionFn(t *testing.T) { } - nrDenom := "nonrestrictedmarker" + nrDenom := "nonrestrictedmarker" // cosmos1kfpnkyu2vln5ywrhhvuwdy5pe9an5krxm09mzm nrMarker := newMarker(nrDenom, coin, nil) // Create a marker similar to the ones we use for hash grants. - gDenom := "grantmarker" + gDenom := "grantmarker" // cosmos1j6pqqrczarugl3zxunvnwgsrq04rzm9wujnlj4 gMarker := newMarkerAcc(gDenom, coin, nil) gMarker.Supply = sdkmath.ZeroInt() gMarker.AccessControl = []types.AccessGrant{ @@ -159,20 +160,21 @@ func TestSendRestrictionFn(t *testing.T) { types.Access_Mint, types.Access_Admin, types.Access_Deposit, types.Access_Withdraw}}} gMarker = createActiveMarker(gMarker) - denomOther := "othercoin" + + denomOther := "othercoin" // cosmos17hpvvkergxjys0mlsdy9s8r2r3hksl3fsffn94 (if it were a marker account). // And throw some other funds in there. require.NoError(t, testutil.FundAccount(types.WithBypass(ctx), app.BankKeeper, gMarker.GetAddress(), cz(c(5000, denomOther))), "Adding funds to %s marker account", gDenom) - rDenomNoAttr := "restrictedmarkernoreqattributes" + rDenomNoAttr := "restrictedmarkernoreqattributes" // cosmos1y4lw4mu35znxeuu00t2waemytpprkv9kazmkp2 rMarkerNoAttr := newMarker(rDenomNoAttr, restricted, nil) app.MarkerKeeper.AddSendDeny(ctx, rMarkerNoAttr.GetAddress(), addrWithDenySend) - rDenom1AttrNoOneHas := "restrictedmarkerreqattributes2" + rDenom1AttrNoOneHas := "restrictedmarkerreqattributes2" // cosmos1up7jx7k307r926gcaqnylnqm9nj9vukc90z6u4 newMarker(rDenom1AttrNoOneHas, restricted, []string{"some.attribute.that.i.require"}) - rDenom1Attr := "restrictedmarkerreqattributes3" + rDenom1Attr := "restrictedmarkerreqattributes3" // cosmos13h8gqljs4yz8v6duauce47aw2cqnz5njf46sl9 rMarker1Attr := newMarker(rDenom1Attr, restricted, []string{"kyc.provenance.io"}) require.NoError(t, app.AttributeKeeper.SetAttribute(ctx, attrTypes.Attribute{ @@ -184,13 +186,13 @@ func TestSendRestrictionFn(t *testing.T) { owner, ), "SetAttribute kyc.provenance.io") - rDenom2Attrs := "restrictedmarkerreqattributes4" + rDenom2Attrs := "restrictedmarkerreqattributes4" // cosmos1qnp4x74tlzz5w9any3etx0erhl92ufk9pfwtv2 rMarker2Attrs := newMarker(rDenom2Attrs, restricted, []string{"kyc.provenance.io", "not-kyc.provenance.io"}) - rDenom3Attrs := "restrictedmarkerreqattributes5" + rDenom3Attrs := "restrictedmarkerreqattributes5" // cosmos15jcvmcvecjxysdemd2t9qvy5wp8ak28z8sgcnn newMarker(rDenom3Attrs, restricted, []string{"kyc.provenance.io", "not-kyc.provenance.io", "foo.provenance.io"}) - rDenomProposed := "stillproposed" + rDenomProposed := "stillproposed" // cosmos1cjq467qkvef5gu4nczt42fl3q8pgmrnesx4647 rMarkerProposed := newProposedMarker(rDenomProposed, restricted, nil) noAccessErr := func(addr sdk.AccAddress, role types.Access, denom string) string { @@ -198,6 +200,15 @@ func TestSendRestrictionFn(t *testing.T) { require.NoError(t, err, "MarkerAddress(%q)", denom) return fmt.Sprintf("%s does not have %s on %s marker (%s)", addr, role, denom, mAddr) } + multiNoAccessErr := func(role types.Access, denom string, addrs ...sdk.AccAddress) string { + mAddr, err := types.MarkerAddress(denom) + require.NoError(t, err, "MarkerAddress(%q)", denom) + strs := make([]string, len(addrs)) + for i, addr := range addrs { + strs[i] = addr.String() + } + return fmt.Sprintf("none of %q have %s on %s marker (%s)", strs, role, denom, mAddr) + } testCases := []struct { name string @@ -225,7 +236,7 @@ func TestSendRestrictionFn(t *testing.T) { { name: "restricted to fee collector from normal account", // include a transfer agent just to make sure that doesn't bypass anything. - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTransfer)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTransfer)), from: addrOther, to: addrFeeCollector, amt: cz(c(1, rDenomNoAttr)), @@ -234,7 +245,7 @@ func TestSendRestrictionFn(t *testing.T) { { name: "restricted to fee collector from marker module account", // include a transfer agent just to make sure that doesn't bypass anything. - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTranWithdraw)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)), from: app.MarkerKeeper.GetMarkerModuleAddr(), to: addrFeeCollector, amt: cz(c(1, rDenomNoAttr)), @@ -243,7 +254,7 @@ func TestSendRestrictionFn(t *testing.T) { { name: "restricted to fee collector from ibc transfer module account", // include a transfer agent just to make sure that doesn't bypass anything. - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTransfer)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTransfer)), from: app.MarkerKeeper.GetIbcTransferModuleAddr(), to: addrFeeCollector, amt: cz(c(1, rDenomNoAttr)), @@ -544,7 +555,7 @@ func TestSendRestrictionFn(t *testing.T) { }, { name: "from marker: admin without withdraw permission", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTransfer)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTransfer)), from: rMarkerNoAttr.GetAddress(), to: addrWithAttrs, amt: cz(c(2, rDenomNoAttr)), @@ -552,7 +563,7 @@ func TestSendRestrictionFn(t *testing.T) { }, { name: "from marker: withdraw marker funds from inactive marker", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTranWithdraw)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)), from: rMarkerProposed.GetAddress(), to: addrWithAttrs, amt: cz(c(2, rDenomNoAttr), c(1, rDenomProposed), c(5, rDenom3Attrs)), @@ -562,35 +573,57 @@ func TestSendRestrictionFn(t *testing.T) { }, { name: "from marker: withdraw non-marker funds from inactive marker", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTranWithdraw)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)), from: rMarkerProposed.GetAddress(), to: addrWithAttrs, amt: cz(c(2, rDenomNoAttr), c(5, rDenom3Attrs)), }, { name: "from marker: withdraw from active marker", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTranWithdraw)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)), from: rMarkerNoAttr.GetAddress(), to: addrWithAttrs, amt: cz(c(3, rDenomNoAttr)), }, { - name: "with admin: does not have transfer: okay otherwise", - ctx: ctxP(types.WithTransferAgent(ctx, addrOther)), + name: "with admin: does not have transfer, but from does", + ctx: ctxP(types.WithTransferAgents(ctx, addrOther)), from: owner, to: addrWithAttrs, amt: cz(c(1, rDenom1Attr), c(1, nrDenom)), }, { name: "with admin: has transfer: would otherwise fail", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTransfer)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTransfer)), + from: addrWithDenySend, + to: addrWithAttrs, + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "with two admins: neither has transfer", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithDeposit, addrWithWithdraw)), + from: addrOther, + to: addrWithAttrs, + amt: cz(c(1, rDenomNoAttr)), + expErr: addrOther.String() + " does not have transfer permissions for " + rDenomNoAttr, + }, + { + name: "with two admins: first has transfer", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTransfer, addrWithWithdraw)), + from: addrWithDenySend, + to: addrWithAttrs, + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "with two admins: second has transfer", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithDeposit, addrWithTransfer)), from: addrWithDenySend, to: addrWithAttrs, amt: cz(c(1, rDenomNoAttr)), }, { name: "from marker to marker: admin only has transfer", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTransfer)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTransfer)), from: rMarkerNoAttr.GetAddress(), to: rMarker1Attr.GetAddress(), amt: cz(c(1, rDenom1AttrNoOneHas)), @@ -598,7 +631,7 @@ func TestSendRestrictionFn(t *testing.T) { }, { name: "from marker to marker: admin only has deposit", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithDeposit)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithDeposit)), from: rMarkerNoAttr.GetAddress(), to: rMarker1Attr.GetAddress(), amt: cz(c(1, rDenom1AttrNoOneHas)), @@ -606,7 +639,7 @@ func TestSendRestrictionFn(t *testing.T) { }, { name: "from marker to marker: admin only has withdraw", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithWithdraw)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithWithdraw)), from: rMarkerNoAttr.GetAddress(), to: rMarker1Attr.GetAddress(), amt: cz(c(1, rDenom1AttrNoOneHas)), @@ -614,7 +647,7 @@ func TestSendRestrictionFn(t *testing.T) { }, { name: "from marker to marker: admin only has transfer and deposit", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTranDep)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTranDep)), from: rMarkerNoAttr.GetAddress(), to: rMarker1Attr.GetAddress(), amt: cz(c(1, rDenom1AttrNoOneHas)), @@ -622,7 +655,7 @@ func TestSendRestrictionFn(t *testing.T) { }, { name: "from marker to marker: admin only has transfer and withdraw", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTranWithdraw)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTranWithdraw)), from: rMarkerNoAttr.GetAddress(), to: rMarker1Attr.GetAddress(), amt: cz(c(1, rDenom1AttrNoOneHas)), @@ -630,15 +663,99 @@ func TestSendRestrictionFn(t *testing.T) { }, { name: "from marker to marker: admin only has deposit and withdraw", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithDepWithdraw)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithDepWithdraw)), from: rMarker1Attr.GetAddress(), to: rMarker2Attrs.GetAddress(), amt: cz(c(1, rDenom1Attr)), - expErr: noAccessErr(addrWithDepWithdraw, types.Access_Transfer, rDenom1Attr), + expErr: multiNoAccessErr(types.Access_Transfer, rDenom1Attr, rMarker1Attr.GetAddress(), addrWithDepWithdraw), }, { name: "from marker to marker: admin has transfer and deposit and withdraw", - ctx: ctxP(types.WithTransferAgent(ctx, addrWithTranDepWithdraw)), + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTranDepWithdraw)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: transfer, deposit, withdraw", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTransfer, addrWithDeposit, addrWithWithdraw)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: transfer, withdraw, deposit", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTransfer, addrWithWithdraw, addrWithDeposit)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: withdraw, transfer, deposit", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithWithdraw, addrWithTransfer, addrWithDeposit)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: withdraw, deposit, transfer", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithWithdraw, addrWithDeposit, addrWithTransfer)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: deposit, withdraw, transfer", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithDeposit, addrWithWithdraw, addrWithTransfer)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: deposit, transfer, withdraw", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithDeposit, addrWithTransfer, addrWithWithdraw)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: trans+dep+withdraw, none, none", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTranDepWithdraw, addrOther, addrOther2)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: none, trans+dep+withdraw, none", + ctx: ctxP(types.WithTransferAgents(ctx, addrOther, addrWithTranDepWithdraw, addrOther2)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: none, none, trans+dep+withdraw", + ctx: ctxP(types.WithTransferAgents(ctx, addrOther, addrOther2, addrWithTranDepWithdraw)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: trans+dep, withdraw, none", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTranDep, addrWithWithdraw, addrOther)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "from marker to marker: 3 admins: none, trans+withdraw, dep", + ctx: ctxP(types.WithTransferAgents(ctx, addrOther, addrWithTranWithdraw, addrWithDeposit)), + from: rMarker1Attr.GetAddress(), + to: rMarker2Attrs.GetAddress(), + amt: cz(c(1, rDenomNoAttr)), + }, + { + name: "trans, none, dep+withdraw", + ctx: ctxP(types.WithTransferAgents(ctx, addrWithTransfer, addrOther, addrWithDepWithdraw)), from: rMarker1Attr.GetAddress(), to: rMarker2Attrs.GetAddress(), amt: cz(c(1, rDenomNoAttr)), diff --git a/x/marker/spec/12_transfers.md b/x/marker/spec/12_transfers.md index b56635a251..03406b8dd6 100644 --- a/x/marker/spec/12_transfers.md +++ b/x/marker/spec/12_transfers.md @@ -109,13 +109,13 @@ flowchart TD qhasbp{{"Does context have bypass, or is the Sender either\nthe marker module or ibc transfer account?"}} qfc{{"Is the Receiver the fee collector?"}} qrc{{"Is there a restricted coin in the Amount?"}} - gta["Get Transfer Agent from the context if possible."] - csm[["checkSenderMarker(Sender, Transfer Agent)"]] + gta["Get Transfer Agents from the context if possible."] + csm[["checkSenderMarker(Sender, Transfer Agents)"]] issmok{{"Proceed?"}} - crm[["checkReceiverMarker(Receiver, Sender, Transfer Agent)"]] + crm[["checkReceiverMarker(Receiver, Sender, Transfer Agents)"]] isrmok{{"Proceed?"}} nextd["Get next Denom from Amount."] - vsd[["validateSendDenom(Sender, Receiver, Denom)"]] + vsd[["validateSendDenom(Sender, Receiver, Denom, Transfer Agents)"]] isdok{{"Is Denom transfer allowed?"}} mored{{"Does Amount have another Denom?"}} ok(["Send allowed."]) @@ -157,11 +157,10 @@ This flow checks that, if this is a withdrawal, nothing (yet) prevents the send. ```mermaid %%{ init: { 'flowchart': { 'curve': 'monotoneY'} } }%% flowchart TD - start[["checkSenderMarker(Sender, Transfer Agent)"]] + start[["checkSenderMarker(Sender, Transfer Agents)"]] issm{{"Is Sender a marker?"}} isfg{{"Is a fee grant in use?"}} - haveta{{"Is there a Transfer Agent?"}} - istaw{{"Does the Transfer Agent\nhave withdraw access?"}} + istaw{{"Does a Transfer Agent\nhave withdraw access?"}} isasm{{"Does the Amount have\nthe Sender marker's denom?"}} issma{{"Is Sender marker active?"}} ok(["Proceed."]) @@ -170,9 +169,7 @@ flowchart TD style denied fill:#ffaaaa,stroke:#b30000,stroke-width:3px start --> issm issm -->|yes| isfg - isfg -->|no| haveta - haveta -->|yes| istaw - haveta -.->|no| denied + isfg -->|no| istaw istaw -.->|no| denied istaw -->|yes| isasm isfg -->|yes| isasm @@ -181,8 +178,8 @@ flowchart TD isasm -.->|no| ok issma -.->|no| denied issm -.->|no| ok - linkStyle 4,5,11 stroke:#b30000,color:#b30000 - linkStyle 9,10,12 stroke:#1b8500,color:#1b8500 + linkStyle 3,9 stroke:#b30000,color:#b30000 + linkStyle 7,8,10 stroke:#1b8500,color:#1b8500 ``` #### checkReceiverMarker @@ -192,11 +189,11 @@ This flow checks that, if this is a deposit, nothing (yet) prevents the send. It ```mermaid %%{ init: { 'flowchart': { 'curve': 'monotoneY'} } }%% flowchart TD - start[["checkReceiverMarker(Receiver, Sender, Transfer Agent)"]] + start[["checkReceiverMarker(Receiver, Sender, Transfer Agents)"]] issm{{"Is Receiver a restricted marker?"}} - haveta{{"Is there a Transfer Agent?"}} + haveta{{"Are there a Transfer Agents?"}} isrd{{"Does Sender\nhave deposit access?"}} - istad{{"Does Transfer Agent\nhave deposit access?"}} + istad{{"Does a Transfer Agent\nhave deposit access?"}} ok(["Proceed."]) style ok fill:#bbffaa,stroke:#1b8500,stroke-width:3px denied(["Send denied."]) @@ -221,7 +218,7 @@ Each `Denom` is checked using `validateSendDenom`, which has this flow. It is us ```mermaid %%{ init: { 'flowchart': { 'curve': 'monotoneY'} } }%% flowchart TD - start[["validateSendDenom(Sender, Receiver, Denom)"]] + start[["validateSendDenom(Sender, Receiver, Denom, Transfer Agents)"]] isdm{{"Is there a marker for Denom?"}} isma{{"Is the marker active?"}} qisrc{{"Is Denom a restricted coin?"}} @@ -331,8 +328,8 @@ If the `Receiver` is a quarantined account, we can assume that it is neither a m ```mermaid %%{ init: { 'flowchart': { 'curve': 'monotoneY'} } }%% flowchart LR - vsd[["validateSendDenom(Sender, Receiver, Denom)"]] - transq{{"Does Sender have\ntransfer for Denom?"}} + vsd[["validateSendDenom(Sender, Receiver, Denom, Transfer Agents)"]] + transq{{"Does Sender or a transfer agent\n have transfer for Denom?"}} mreqattr{{"Does Denom have\nrequired attributes?"}} treqattr{{"Does Receiver have\nthose attributes?"}} ok(["Denom transfer allowed."]) diff --git a/x/marker/types/marker.go b/x/marker/types/marker.go index 888455c314..ff9f0d3574 100644 --- a/x/marker/types/marker.go +++ b/x/marker/types/marker.go @@ -160,6 +160,31 @@ func (ma *MarkerAccount) ValidateAddressHasAccess(addr sdk.AccAddress, role Acce return ma.ValidateHasAccess(addr.String(), role) } +// AtLeastOneAddrHasAccess returns true if one or more of the provided addrs has the given role on this marker. +func AtLeastOneAddrHasAccess(ma MarkerAccountI, addrs []sdk.AccAddress, role Access) bool { + for _, addr := range addrs { + if ma.HasAccess(addr.String(), role) { + return true + } + } + return false +} + +// ValidateAtLeastOneAddrHasAccess returns an error if there isn't an entry in addrs that has the given role in this marker. +func ValidateAtLeastOneAddrHasAccess(ma MarkerAccountI, addrs []sdk.AccAddress, role Access) error { + if len(addrs) == 1 { + return ma.ValidateHasAccess(addrs[0].String(), role) + } + if !AtLeastOneAddrHasAccess(ma, addrs, role) { + strs := make([]string, len(addrs)) + for i, addr := range addrs { + strs[i] = addr.String() + } + return fmt.Errorf("none of %q have permission %s on %s marker (%s)", strs, role, ma.GetDenom(), ma.GetAddress()) + } + return nil +} + // AddressListForPermission returns a list of all addresses with the provided rule within the // current MarkerAccount AccessControl list func (ma *MarkerAccount) AddressListForPermission(role Access) []sdk.AccAddress { diff --git a/x/marker/types/marker_test.go b/x/marker/types/marker_test.go index 643a3f8826..d1406d4fa5 100644 --- a/x/marker/types/marker_test.go +++ b/x/marker/types/marker_test.go @@ -15,10 +15,6 @@ import ( "github.com/provenance-io/provenance/testutil/assertions" ) -func init() { - -} - func accAddressFromBech32(t *testing.T, addrStr string) sdk.AccAddress { addr, err := sdk.AccAddressFromBech32(addrStr) require.NoError(t, err) @@ -575,3 +571,115 @@ func TestHasAccess(t *testing.T) { }) } } + +func TestValidateAtLeastOneAddrHasAccess(t *testing.T) { + addr1 := sdk.AccAddress("1_addr______________") + addr2 := sdk.AccAddress("2_addr______________") + addr3 := sdk.AccAddress("3_addr______________") + + denom := "moomoo" + ma := &MarkerAccount{ + BaseAccount: &authtypes.BaseAccount{Address: MustGetMarkerAddress(denom).String()}, + Denom: denom, + AccessControl: []AccessGrant{ + {Address: addr1.String(), Permissions: AccessList{Access_Mint, Access_Burn}}, + {Address: addr2.String(), Permissions: AccessList{Access_Deposit, Access_Withdraw}}, + {Address: addr3.String(), Permissions: AccessList{Access_Transfer, Access_Admin, Access_Delete}}, + }, + } + markerDesc := denom + " marker (" + ma.BaseAccount.Address + ")" + + addrOther1 := sdk.AccAddress("one_other_addr______") + addrOther2 := sdk.AccAddress("two_other_addr______") + addrOther3 := sdk.AccAddress("three_other_addr____") + + tests := []struct { + name string + addrs []sdk.AccAddress + role Access + exp string + }{ + { + name: "nil addrs", + addrs: nil, + role: Access_Mint, + exp: "none of [] have permission ACCESS_MINT on " + markerDesc, + }, + { + name: "empty addrs", + addrs: []sdk.AccAddress{}, + role: Access_Mint, + exp: "none of [] have permission ACCESS_MINT on " + markerDesc, + }, + { + name: "one addr: no perms", + addrs: []sdk.AccAddress{addrOther1}, + role: Access_Burn, + exp: addrOther1.String() + " does not have ACCESS_BURN on " + markerDesc, + }, + { + name: "one addr: unknown role", + addrs: []sdk.AccAddress{addr1}, + role: 55, + exp: addr1.String() + " does not have 55 on " + markerDesc, + }, + { + name: "one addr: other role", + addrs: []sdk.AccAddress{addr1}, + role: Access_ForceTransfer, + exp: addr1.String() + " does not have ACCESS_FORCE_TRANSFER on " + markerDesc, + }, + { + name: "one addr: has role", + addrs: []sdk.AccAddress{addr1}, + role: Access_Mint, + exp: "", + }, + { + name: "three addrs: no match", + addrs: []sdk.AccAddress{addrOther1, addrOther2, addrOther3}, + role: Access_Withdraw, + exp: "none of [\"" + addrOther1.String() + "\" \"" + addrOther2.String() + "\" \"" + + addrOther3.String() + "\"] have permission ACCESS_WITHDRAW on " + markerDesc, + }, + { + name: "three addrs: match first", + addrs: []sdk.AccAddress{addr1, addrOther2, addrOther3}, + role: Access_Burn, + exp: "", + }, + { + name: "three addrs: match second", + addrs: []sdk.AccAddress{addrOther1, addr2, addrOther3}, + role: Access_Deposit, + exp: "", + }, + { + name: "three addrs: match third", + addrs: []sdk.AccAddress{addrOther1, addrOther2, addr3}, + role: Access_Admin, + exp: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + expIs := len(tc.exp) == 0 + var actIs bool + testIsFunc := func() { + actIs = AtLeastOneAddrHasAccess(ma, tc.addrs, tc.role) + } + if assert.NotPanics(t, testIsFunc, "AtLeastOneAddrHasAccess") { + assert.Equal(t, expIs, actIs, "result from AtLeastOneAddrHasAccess") + } + + var err error + testValFunc := func() { + err = ValidateAtLeastOneAddrHasAccess(ma, tc.addrs, tc.role) + } + if assert.NotPanics(t, testValFunc, "ValidateAtLeastOneAddrHasAccess") { + assertions.AssertErrorValue(t, err, tc.exp, "result from ValidateAtLeastOneAddrHasAccess") + } + }) + } +} diff --git a/x/marker/types/send_restrictions.go b/x/marker/types/send_restrictions.go index 446a352fed..703fed793e 100644 --- a/x/marker/types/send_restrictions.go +++ b/x/marker/types/send_restrictions.go @@ -8,7 +8,7 @@ import ( var ( bypassKey = "bypass-marker-restriction" - transferAgentKey = "marker-transfer-agent" + transferAgentKey = "marker-transfer-agents" ) // WithBypass returns a new context that will cause the marker bank send restriction to be skipped. @@ -36,27 +36,28 @@ func HasBypass[C context.Context](ctx C) bool { return isBool && bypass } -// WithTransferAgent returns a new context that contains the provided marker transfer agent. -func WithTransferAgent[C context.Context](ctx C, transferAgent sdk.AccAddress) C { +// WithTransferAgents returns a new context that contains the provided marker transfer agent. +// This will overwrite any existing transfer agents in the context. +func WithTransferAgents[C context.Context](ctx C, transferAgents ...sdk.AccAddress) C { sdkCtx := sdk.UnwrapSDKContext(ctx) - sdkCtx = sdkCtx.WithValue(transferAgentKey, transferAgent) + sdkCtx = sdkCtx.WithValue(transferAgentKey, transferAgents) return context.Context(sdkCtx).(C) } -// WithoutTransferAgent returns a new context with a nil marker transfer agent. -func WithoutTransferAgent[C context.Context](ctx C) C { +// WithoutTransferAgents returns a new context without any marker transfer agents. +func WithoutTransferAgents[C context.Context](ctx C) C { sdkCtx := sdk.UnwrapSDKContext(ctx) - sdkCtx = sdkCtx.WithValue(transferAgentKey, sdk.AccAddress(nil)) + sdkCtx = sdkCtx.WithValue(transferAgentKey, []sdk.AccAddress(nil)) return context.Context(sdkCtx).(C) } -// GetTransferAgent gets the marker transfer agent from the provided context. -func GetTransferAgent[C context.Context](ctx C) sdk.AccAddress { +// GetTransferAgents gets the marker transfer agents from the provided context. +func GetTransferAgents[C context.Context](ctx C) []sdk.AccAddress { sdkCtx := sdk.UnwrapSDKContext(ctx) val := sdkCtx.Value(transferAgentKey) if val == nil { return nil } - rv, _ := val.(sdk.AccAddress) + rv, _ := val.([]sdk.AccAddress) return rv } diff --git a/x/marker/types/send_restrictions_test.go b/x/marker/types/send_restrictions_test.go index 75231f56c5..b7ada9d8ea 100644 --- a/x/marker/types/send_restrictions_test.go +++ b/x/marker/types/send_restrictions_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -24,35 +25,42 @@ func TestContextCombos(t *testing.T) { name string ctx sdk.Context expBypass bool - expTA sdk.AccAddress + expTAs []sdk.AccAddress }{ { name: "with transfer agent on with bypass", - ctx: WithTransferAgent(WithBypass(newCtx()), sdk.AccAddress("some_transfer_agent_")), + ctx: WithTransferAgents(WithBypass(newCtx()), sdk.AccAddress("some_transfer_agent_")), expBypass: true, - expTA: sdk.AccAddress("some_transfer_agent_"), + expTAs: []sdk.AccAddress{sdk.AccAddress("some_transfer_agent_")}, }, { - name: "with bypass on with transfer agent", - ctx: WithBypass(WithTransferAgent(newCtx(), sdk.AccAddress("other_transfer_agent"))), + name: "with bypass on with transfer agents", + ctx: WithBypass(WithTransferAgents(newCtx(), sdk.AccAddress("other_transfer_agent"), sdk.AccAddress("third_transfer_agent"))), expBypass: true, - expTA: sdk.AccAddress("other_transfer_agent"), + expTAs: []sdk.AccAddress{sdk.AccAddress("other_transfer_agent"), sdk.AccAddress("third_transfer_agent")}, }, { name: "without either on with transfer agent and bypass", - ctx: WithoutBypass(WithoutTransferAgent(WithBypass(WithTransferAgent(newCtx(), sdk.AccAddress("bad_transfer_agent__"))))), + ctx: WithoutBypass(WithoutTransferAgents(WithBypass(WithTransferAgents(newCtx(), sdk.AccAddress("bad_transfer_agent__"))))), expBypass: false, - expTA: nil, + expTAs: nil, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - actBypass := HasBypass(tc.ctx) - actTA := GetTransferAgent(tc.ctx) - + var actBypass bool + testHasBypass := func() { + actBypass = HasBypass(tc.ctx) + } + require.NotPanics(t, testHasBypass, "HasBypass") + var actTAs []sdk.AccAddress + testGetTransferAgents := func() { + actTAs = GetTransferAgents(tc.ctx) + } + require.NotPanics(t, testGetTransferAgents, "GetTransferAgents") assert.Equal(t, tc.expBypass, actBypass, "HasBypass") - assert.Equal(t, tc.expTA, actTA, "GetTransferAgent") + assert.Equal(t, tc.expTAs, actTAs, "GetTransferAgents") }) } } @@ -102,7 +110,11 @@ func TestBypassFuncs(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - actual := HasBypass(tc.ctx) + var actual bool + testFunc := func() { + actual = HasBypass(tc.ctx) + } + require.NotPanics(t, testFunc, "HasBypass") assert.Equal(t, tc.exp, actual, "HasBypass") }) } @@ -128,7 +140,7 @@ func TestTransferAgentFuncs(t *testing.T) { tests := []struct { name string ctx sdk.Context - exp sdk.AccAddress + exp []sdk.AccAddress }{ { name: "brand new mostly empty context", @@ -136,56 +148,103 @@ func TestTransferAgentFuncs(t *testing.T) { exp: nil, }, { - name: "context with transfer agent", - ctx: WithTransferAgent(newCtx(), sdk.AccAddress("transfer_agent______")), - exp: sdk.AccAddress("transfer_agent______"), + name: "one transfer agent", + ctx: WithTransferAgents(newCtx(), sdk.AccAddress("transfer_agent______")), + exp: []sdk.AccAddress{sdk.AccAddress("transfer_agent______")}, + }, + { + name: "two transfer agents", + ctx: WithTransferAgents(newCtx(), sdk.AccAddress("transfer_agent_one__"), sdk.AccAddress("transfer_agent_two__")), + exp: []sdk.AccAddress{sdk.AccAddress("transfer_agent_one__"), sdk.AccAddress("transfer_agent_two__")}, }, { - name: "context without transfer agent", - ctx: WithoutTransferAgent(newCtx()), + name: "without transfer agents", + ctx: WithoutTransferAgents(newCtx()), exp: nil, }, { - name: "context with transfer agent twice", - ctx: WithTransferAgent(WithTransferAgent(newCtx(), sdk.AccAddress("first_transfer_agent")), sdk.AccAddress("agent_2_of_transfer_")), - exp: sdk.AccAddress("agent_2_of_transfer_"), + name: "one transfer agent on context already with one", + ctx: WithTransferAgents( + WithTransferAgents(newCtx(), sdk.AccAddress("first_transfer_agent")), + sdk.AccAddress("agent_2_of_transfer_"), + ), + exp: []sdk.AccAddress{sdk.AccAddress("agent_2_of_transfer_")}, + }, + { + name: "two transfer agents on context already with one", + ctx: WithTransferAgents( + WithTransferAgents(newCtx(), sdk.AccAddress("first_transfer_agent")), + sdk.AccAddress("agent_of_transfer_2_"), sdk.AccAddress("agent_of_chaos______"), + ), + exp: []sdk.AccAddress{sdk.AccAddress("agent_of_transfer_2_"), sdk.AccAddress("agent_of_chaos______")}, + }, + { + name: "one transfer agent on context already with two", + ctx: WithTransferAgents( + WithTransferAgents(newCtx(), sdk.AccAddress("1st_transfer_agent__"), sdk.AccAddress("2nd_transfer_agent__")), + sdk.AccAddress("another_agent_______"), + ), + exp: []sdk.AccAddress{sdk.AccAddress("another_agent_______")}, + }, + { + name: "two transfer agents on context already with two", + ctx: WithTransferAgents( + WithTransferAgents(newCtx(), sdk.AccAddress("transfer_agent_one__"), sdk.AccAddress("transfer_agent_two__")), + sdk.AccAddress("transfer_agent_three"), sdk.AccAddress("transfer_agent_four_"), + ), + exp: []sdk.AccAddress{sdk.AccAddress("transfer_agent_three"), sdk.AccAddress("transfer_agent_four_")}, }, { name: "context without transfer agent twice", - ctx: WithoutTransferAgent(WithoutTransferAgent(newCtx())), + ctx: WithoutTransferAgents(WithoutTransferAgents(newCtx())), exp: nil, }, { - name: "context with transfer agent on one that originally was without it", - ctx: WithTransferAgent(WithoutTransferAgent(newCtx()), sdk.AccAddress("agent_of_transfer___")), - exp: sdk.AccAddress("agent_of_transfer___"), + name: "context with one transfer agent on one that originally was without it", + ctx: WithTransferAgents(WithoutTransferAgents(newCtx()), sdk.AccAddress("agent_of_transfer___")), + exp: []sdk.AccAddress{sdk.AccAddress("agent_of_transfer___")}, + }, + { + name: "context with two transfer agent on one that originally was without it", + ctx: WithTransferAgents(WithoutTransferAgents(newCtx()), sdk.AccAddress("agent_of_transfer_1_"), sdk.AccAddress("agent_of_transfer_2_")), + exp: []sdk.AccAddress{sdk.AccAddress("agent_of_transfer_1_"), sdk.AccAddress("agent_of_transfer_2_")}, + }, + { + name: "context without transfer agent on one that originally had one", + ctx: WithoutTransferAgents(WithTransferAgents(newCtx(), sdk.AccAddress("the_transfer_agent__"))), + exp: nil, }, { - name: "context without transfer agent on one that originally had it", - ctx: WithoutTransferAgent(WithTransferAgent(newCtx(), sdk.AccAddress("the_transfer_agent__"))), + name: "context without transfer agent on one that originally had two", + ctx: WithoutTransferAgents(WithTransferAgents(newCtx(), sdk.AccAddress("the_transfer_agent__"), sdk.AccAddress("other_transfer_agent"))), exp: nil, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - actual := GetTransferAgent(tc.ctx) - assert.Equal(t, tc.exp, actual, "GetTransferAgent") + var actual []sdk.AccAddress + testFunc := func() { + actual = GetTransferAgents(tc.ctx) + } + require.NotPanics(t, testFunc, "GetTransferAgents") + assert.Equal(t, tc.exp, actual, "GetTransferAgents") }) } } func TestTransferAgentFuncsDoNotModifyProvided(t *testing.T) { origCtx := sdk.NewContext(nil, cmtproto.Header{}, false, nil) - assert.Nil(t, GetTransferAgent(origCtx), "GetTransferAgent(origCtx)") + assert.Nil(t, GetTransferAgents(origCtx), "GetTransferAgents(origCtx)") ta := sdk.AccAddress("great_transfer_agent") - afterWith := WithTransferAgent(origCtx, ta) - assert.Equal(t, ta, GetTransferAgent(afterWith), "GetTransferAgent(afterWith)") - assert.Nil(t, GetTransferAgent(origCtx), "GetTransferAgent(origCtx) after giving it to WithTransferAgent") - - afterWithout := WithoutTransferAgent(afterWith) - assert.Nil(t, GetTransferAgent(afterWithout), "GetTransferAgent(afterWithout)") - assert.Equal(t, ta, GetTransferAgent(afterWith), "GetTransferAgent(afterWith) after giving it to WithoutTransferAgent") - assert.Nil(t, GetTransferAgent(origCtx), "GetTransferAgent(origCtx) after giving afterWith to WithoutTransferAgent") + expAgents := []sdk.AccAddress{ta} + afterWith := WithTransferAgents(origCtx, ta) + assert.Equal(t, expAgents, GetTransferAgents(afterWith), "GetTransferAgents(afterWith)") + assert.Nil(t, GetTransferAgents(origCtx), "GetTransferAgents(origCtx) after giving it to WithTransferAgents") + + afterWithout := WithoutTransferAgents(afterWith) + assert.Nil(t, GetTransferAgents(afterWithout), "GetTransferAgents(afterWithout)") + assert.Equal(t, expAgents, GetTransferAgents(afterWith), "GetTransferAgents(afterWith) after giving it to WithoutTransferAgents") + assert.Nil(t, GetTransferAgents(origCtx), "GetTransferAgents(origCtx) after giving afterWith to WithoutTransferAgents") } diff --git a/x/metadata/client/cli/cli_page_test.go b/x/metadata/client/cli/cli_page_test.go index 66070aa967..51789b5c5f 100644 --- a/x/metadata/client/cli/cli_page_test.go +++ b/x/metadata/client/cli/cli_page_test.go @@ -170,15 +170,15 @@ func (s *IntegrationCLIPageTestSuite) SetupSuite() { // i % 5 == 3: 20 of them are owned by s.accountAddr + s.user1Addr and value owner is s.user1Addr. // i % 5 == 4: 20 of them are owned by s.user1Addr and value owner is s.user1Addr. // Result: - // s.user1Addr is owner of 80 (either owner or value owner or data access) - // s.accountAddr is owner of 80 (either owner or value owner or data access) - // s.user1Addr is value owner of 60 (just looking at value owner field) - // s.accountAddr is value owner of 40 (just looking at value owner field) + // s.user1Addr is in the Owners field of 60. + // s.accountAddr is in the Owners field of 80. + // s.user1Addr is the value owner of 60. + // s.accountAddr is the value owner of 40. // Sessions: // Use each c spec on the scope spec // Records: // Use each record spec in the contract spec - s.user1ScopesOwned = 80 + s.user1ScopesOwned = 60 s.accountScopesOwned = 80 s.user1ScopesValueOwned = 60 s.accountScopesValueOwned = 40 diff --git a/x/metadata/client/cli/cli_test.go b/x/metadata/client/cli/cli_test.go index ded2b5f24d..33b6bffcf3 100644 --- a/x/metadata/client/cli/cli_test.go +++ b/x/metadata/client/cli/cli_test.go @@ -1576,7 +1576,7 @@ func (s *IntegrationCLITestSuite) TestGetOwnershipCmd() { { name: "scope through value owner", args: []string{s.user2AddrStr}, - expOut: []string{scopeUUIDsText}, + expOut: []string{"scope_uuids: []", "total: \"0\""}, }, { name: "no result", @@ -2233,7 +2233,7 @@ func (s *IntegrationCLITestSuite) TestUpdateMigrateValueOwnersCmds() { args: []string{ s.user1AddrStr, scopeID1, scopeSpecID, }, - expectErrMsg: fmt.Sprintf("invalid scope id %d %q: %s", 2, scopeSpecID, "not a scope identifier"), + expectErrMsg: "not a scope identifier: \"" + scopeSpecID + "\"", }, { name: "update: invalid signers", @@ -2380,6 +2380,7 @@ func (s *IntegrationCLITestSuite) TestUpdateMigrateValueOwnersCmds() { expectedCode: 18, }, }, + queries: queryTests(s.user1AddrStr, s.user1AddrStr, s.user2AddrStr), }, { // A single update of two scopes. diff --git a/x/metadata/client/cli/tx.go b/x/metadata/client/cli/tx.go index 1c5357d5b0..578bbcaece 100644 --- a/x/metadata/client/cli/tx.go +++ b/x/metadata/client/cli/tx.go @@ -325,12 +325,12 @@ func UpdateValueOwnersCmd() *cobra.Command { msg.ScopeIds = make([]types.MetadataAddress, len(args[1:])) for i, arg := range args[1:] { msg.ScopeIds[i], err = types.MetadataAddressFromBech32(arg) - if err == nil && !msg.ScopeIds[i].IsScopeAddress() { - err = fmt.Errorf("not a scope identifier") - } if err != nil { return fmt.Errorf("invalid scope id %d %q: %w", i+1, arg, err) } + if !msg.ScopeIds[i].IsScopeAddress() { + return fmt.Errorf("not a scope identifier: %q", arg) + } } msg.Signers, err = parseSigners(cmd, &clientCtx) diff --git a/x/metadata/keeper/account_data_test.go b/x/metadata/keeper/account_data_test.go index d8334f047c..ce6418c618 100644 --- a/x/metadata/keeper/account_data_test.go +++ b/x/metadata/keeper/account_data_test.go @@ -8,14 +8,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simapp "github.com/provenance-io/provenance/app" - "github.com/provenance-io/provenance/x/metadata/keeper" + "github.com/provenance-io/provenance/testutil/assertions" "github.com/provenance-io/provenance/x/metadata/types" ) -func FreshCtx(app *simapp.App) sdk.Context { - return keeper.AddAuthzCacheToContext(app.BaseApp.NewContext(false)) -} - func TestValidateSetAccountData(t *testing.T) { app := simapp.Setup(t) @@ -80,7 +76,7 @@ func TestValidateSetAccountData(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err := app.MetadataKeeper.ValidateSetAccountData(FreshCtx(app), tc.msg) - AssertErrorValue(t, err, tc.exp, "ValidateSetAccountData") + assertions.AssertErrorValue(t, err, tc.exp, "ValidateSetAccountData") }) } } diff --git a/x/metadata/keeper/bank.go b/x/metadata/keeper/bank.go new file mode 100644 index 0000000000..768fefeabb --- /dev/null +++ b/x/metadata/keeper/bank.go @@ -0,0 +1,87 @@ +package keeper + +import ( + "context" + "fmt" + + "cosmossdk.io/collections" + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + + "github.com/provenance-io/provenance/x/metadata/types" +) + +// scopeDenomPrefix is the string that will start every scope denom. +const scopeDenomPrefix = types.DenomPrefix + types.PrefixScope + "1" + +func NewMDBankKeeper(bk bankkeeper.BaseKeeper) *MDBankKeeper { + return &MDBankKeeper{BaseKeeper: bk} +} + +// MDBankKeeper extends the SDK's bank keeper to add methods that act on fields so that we can mock all of it. +type MDBankKeeper struct { + bankkeeper.BaseKeeper +} + +// DenomOwner gets the singular owner of a denom. +// An error is returned if more than one account owns some of the denom. +// If no one owns the denom, this will return nil, nil. +func (k *MDBankKeeper) DenomOwner(ctx context.Context, denom string) (sdk.AccAddress, error) { + var rv sdk.AccAddress + ranger := collections.NewPrefixedPairRange[string, sdk.AccAddress](denom) + err := k.Balances.Indexes.Denom.Walk(ctx, ranger, func(_ string, addr sdk.AccAddress) (bool, error) { + if len(rv) > 0 { + return true, fmt.Errorf("denom %q has more than one owner", denom) + } + rv = addr + return false, nil + }) + if err != nil { + return nil, err + } + return rv, nil +} + +// GetScopesForValueOwner will get the scopes owned by a specific value owner. +// If the pageReq is nil, this will get all their scopes and the resulting PageResponse will be nil. +// If a pageReq is provided, this will get just the requested page and it will return a PageResponse. +func (k *MDBankKeeper) GetScopesForValueOwner(ctx context.Context, valueOwner sdk.AccAddress, pageReq *query.PageRequest) (types.AccMDLinks, *query.PageResponse, error) { + pfx := collections.Join(valueOwner, scopeDenomPrefix) + + if pageReq != nil { + return query.CollectionPaginate(ctx, k.Balances, pageReq, + func(key collections.Pair[sdk.AccAddress, string], _ sdkmath.Int) (*types.AccMDLink, error) { + return k.balanceValueOwnerTransformer(key), nil + }, + func(o *query.CollectionsPaginateOptions[collections.Pair[sdk.AccAddress, string]]) { + o.Prefix = &pfx + }, + ) + } + + ranger := &collections.Range[collections.Pair[sdk.AccAddress, string]]{} + ranger.Prefix(pfx) + var links types.AccMDLinks + err := k.Balances.Walk(ctx, ranger, func(key collections.Pair[sdk.AccAddress, string], _ sdkmath.Int) (bool, error) { + links = append(links, k.balanceValueOwnerTransformer(key)) + return false, nil + }) + + return links, nil, err +} + +// balanceValueOwnerTransformer creates an AccMDLink from data in the key. If the denom in the key is not a +// metadata denom, an error is written to the logs and the resulting AccMDLink will not have an MDAddr. +func (k *MDBankKeeper) balanceValueOwnerTransformer(key collections.Pair[sdk.AccAddress, string]) *types.AccMDLink { + accAddr := key.K1() + denom := key.K2() + mdAddr, err := types.MetadataAddressFromDenom(denom) + if err != nil { + // MetadataAddressFromDenom always includes the denom in the error message, so we don't need it again here. + k.Logger().Error(fmt.Sprintf("invalid metadata balance entry for account %q: %v", accAddr.String(), err)) + } + return types.NewAccMDLink(accAddr, mdAddr) +} diff --git a/x/metadata/keeper/bank_test.go b/x/metadata/keeper/bank_test.go new file mode 100644 index 0000000000..2ea3f1367e --- /dev/null +++ b/x/metadata/keeper/bank_test.go @@ -0,0 +1,561 @@ +package keeper_test + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + + "cosmossdk.io/collections" + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + + "github.com/provenance-io/provenance/app" + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/testutil/testlog" + "github.com/provenance-io/provenance/x/metadata/keeper" + "github.com/provenance-io/provenance/x/metadata/types" +) + +func TestBankTestSuite(t *testing.T) { + suite.Run(t, new(BankTestSuite)) +} + +type BankTestSuite struct { + suite.Suite + + app *app.App + ctx sdk.Context + bk *keeper.MDBankKeeper + + logBuffer bytes.Buffer +} + +func (s *BankTestSuite) SetupTest() { + // Swap in the buffered logger maker so it's used in app.Setup, but then put it back (since that's a global thing). + defer app.SetLoggerMaker(app.SetLoggerMaker(app.BufferedInfoLoggerMaker(&s.logBuffer))) + s.app = app.Setup(s.T()) + s.logBuffer.Reset() + s.ctx = s.app.NewContext(false) + s.bk = keeper.NewMDBankKeeper(s.app.BankKeeper) +} + +// getLogOutput gets the log buffer contents and logs that to the test. +// The returned value is the contents split on newline with empty lines removed from the end. +// This (probably) also clears out the log buffer. +func (s *BankTestSuite) getLogOutput(msg string, args ...interface{}) []string { + logOutput := s.logBuffer.String() + if len(strings.TrimSpace(logOutput)) == 0 { + s.T().Logf(msg+" log output: ", args...) + return nil + } + s.T().Logf(msg+" log output:\n%s", append(args, logOutput)...) + + rv := strings.Split(logOutput, "\n") + for len(rv) > 0 && len(rv[len(rv)-1]) == 0 { + rv = rv[:len(rv)-1] + } + if len(rv) == 0 { + return nil + } + return rv +} + +func (s *BankTestSuite) AssertErrorValue(theError error, errorString string, msgAndArgs ...interface{}) bool { + s.T().Helper() + return assertions.AssertErrorValue(s.T(), theError, errorString, msgAndArgs...) +} + +type balance struct { + addr sdk.AccAddress + denom string + amt int64 // Defaults to 1 if not provided. +} + +// setBalances adds the provided balances data to the bank keeper's Balances collection. +func (s *BankTestSuite) setBalances(ctx sdk.Context, balances []balance) { + for _, bal := range balances { + amt := sdkmath.OneInt() + if bal.amt > 0 { + amt = sdkmath.NewInt(bal.amt) + } + s.Require().NoError(s.bk.Balances.Set(ctx, collections.Join(bal.addr, bal.denom), amt), + "s.bk.Balances.Set(ctx, collections.Join(%q, %q), %s)", + bal.addr.String(), bal.denom, amt) + } +} + +// parseUUID parses the provided id into a UUID, requiring it to be successful. +func (s *BankTestSuite) parseUUID(uid string) uuid.UUID { + rv, err := uuid.Parse(uid) + s.Require().NoError(err, "uuid.Parse(%q)", uid) + return rv +} + +// scopeID creates a scope id from the provided uuid string. +func (s *BankTestSuite) scopeID(uid string) types.MetadataAddress { + id := s.parseUUID(uid) + return types.ScopeMetadataAddress(id) +} + +// scopeSpecID creates a scope spec id from the provided uuid string. +func (s *BankTestSuite) scopeSpecID(uid string) types.MetadataAddress { + id := s.parseUUID(uid) + return types.ScopeSpecMetadataAddress(id) +} + +func (s *BankTestSuite) TestDenomOwner() { + addr1 := sdk.AccAddress("1_addr______________") // cosmos1x90kzerywf047h6lta047h6lta047h6l258ny6 + addr2 := sdk.AccAddress("2_addr______________") // cosmos1xf0kzerywf047h6lta047h6lta047h6lgww49l + addr3 := sdk.AccAddress("3_addr______________") // cosmos1xd0kzerywf047h6lta047h6lta047h6l3lfhau + addr4 := sdk.AccAddress("4_addr______________") // cosmos1x30kzerywf047h6lta047h6lta047h6lvnue84 + testlog.WriteVariables(s.T(), "addresses", + "addr1", addr1, + "addr2", addr2, + "addr3", addr3, + "addr4", addr4, + ) + + // subOne reduces the last character by 1 (ignoring overflow). + subOne := func(val string) string { + return val[:len(val)-1] + string(val[len(val)-1]-1) + } + // addOne increases the last character by 1 (ignoring overflow). + addOne := func(val string) string { + return val[:len(val)-1] + string(val[len(val)-1]+1) + } + + scopeID := s.scopeID("69012AF4-2FA4-44DA-BAE4-1C13480362C9") // scope1qp5sz2h597jyfk46uswpxjqrvtys3y0ghw + scopeDenom := scopeID.Denom() // nft/scope1qp5sz2h597jyfk46uswpxjqrvtys3y0ghw + scopeDenomBefore := subOne(scopeDenom) // nft/scope1qp5sz2h597jyfk46uswpxjqrvtys3y0ghv + scopeDenomAfter := addOne(scopeDenom) // nft/scope1qp5sz2h597jyfk46uswpxjqrvtys3y0ghx + testlog.WriteVariables(s.T(), "ids and denoms", + "scopeID", scopeID, + "scopeDenom", scopeDenom, + "scopeDenomBefore", scopeDenomBefore, + "scopeDenomAfter", scopeDenomAfter, + ) + + tests := []struct { + name string + balances []balance + denom string + expAddr sdk.AccAddress + expErr string + }{ + { + name: "no owner", + balances: []balance{ + {addr: addr1, denom: scopeDenomBefore}, + {addr: addr3, denom: scopeDenomAfter}, + }, + denom: scopeDenom, + expAddr: nil, + expErr: "", + }, + { + name: "one owner", + balances: []balance{ + {addr: addr1, denom: scopeDenomBefore}, + {addr: addr2, denom: scopeDenom}, + {addr: addr3, denom: scopeDenomAfter}, + }, + denom: scopeDenom, + expAddr: addr2, + expErr: "", + }, + { + name: "two owners", + balances: []balance{ + {addr: addr1, denom: scopeDenomBefore}, + {addr: addr2, denom: scopeDenom}, + {addr: addr3, denom: scopeDenom}, + {addr: addr4, denom: scopeDenomAfter}, + }, + denom: scopeDenom, + expAddr: nil, + expErr: "denom \"" + scopeDenom + "\" has more than one owner", + }, + { + name: "three owners", + balances: []balance{ + {addr: addr1, denom: scopeDenom}, + {addr: addr2, denom: scopeDenom}, + {addr: addr3, denom: scopeDenom}, + }, + denom: scopeDenom, + expAddr: nil, + expErr: "denom \"" + scopeDenom + "\" has more than one owner", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + // Use a cache context for each test so that the setup doesn't persist between tests. + ctx, _ := s.ctx.CacheContext() + s.setBalances(ctx, tc.balances) + + var addr sdk.AccAddress + var err error + testFunc := func() { + addr, err = s.bk.DenomOwner(ctx, tc.denom) + } + s.Require().NotPanics(testFunc, "DenomOwner(%q)", tc.denom) + s.AssertErrorValue(err, tc.expErr, "error returned by DenomOwner(%q)", tc.denom) + s.Assert().Equal(tc.expAddr, addr, "AccAddress returned by DenomOwner(%q)", tc.denom) + }) + } +} + +func (s *BankTestSuite) TestGetScopesForValueOwner() { + addr1 := sdk.AccAddress("1_addr______________") // cosmos1x90kzerywf047h6lta047h6lta047h6l258ny6 + addr2 := sdk.AccAddress("2_addr______________") // cosmos1xf0kzerywf047h6lta047h6lta047h6lgww49l + addr3 := sdk.AccAddress("3_addr______________") // cosmos1xd0kzerywf047h6lta047h6lta047h6l3lfhau + testlog.WriteVariables(s.T(), "addresses", + "addr1", addr1, + "addr2", addr2, + "addr3", addr3, + ) + + scopeID1 := s.scopeID("4CDFD0C4-F08C-403E-A8F7-EC723E7A0001") // scope1qpxdl5xy7zxyq04g7lk8y0n6qqqspk95yz + scopeID2 := s.scopeID("4CDFD0C4-F08C-403E-A8F7-EC723E7A0002") // scope1qpxdl5xy7zxyq04g7lk8y0n6qqpqm2zk6h + scopeID3 := s.scopeID("4CDFD0C4-F08C-403E-A8F7-EC723E7A0003") // scope1qpxdl5xy7zxyq04g7lk8y0n6qqpswt2w0y + scopeID4 := s.scopeID("4CDFD0C4-F08C-403E-A8F7-EC723E7A0004") // scope1qpxdl5xy7zxyq04g7lk8y0n6qqzq2yn38a + scopeID5 := s.scopeID("4CDFD0C4-F08C-403E-A8F7-EC723E7A0005") // scope1qpxdl5xy7zxyq04g7lk8y0n6qqzsl9mfjw + // Note that when sorted by denom, they have this order: scopeID2, scopeID3, scopeID1, scopeID4, scopeID5. + // I'm including tests involving a scope spec denom because scope denoms start with "nft/scope" and scope spec + // denoms would start with "nft/scopespec". The prefix being used should include the "1" that separates the HRP + // and bytes in a bech32 address string. But if the prefix does not have that "1", a scope spec entry would + // end up being included in the results (which we don't want). + scopeSpecID := s.scopeSpecID("4CDFD0C4-F08C-403E-A8F7-EC723E7A0001") // scopespec1q3xdl5xy7zxyq04g7lk8y0n6qqqs0su6rh + testlog.WriteVariables(s.T(), "ids", + "scopeID1", scopeID1, + "scopeID2", scopeID2, + "scopeID3", scopeID3, + "scopeID4", scopeID4, + "scopeID5", scopeID5, + "scopeSpecID", scopeSpecID, + ) + + logMsg := func(owner sdk.AccAddress, denom, err string) string { + return "ERR invalid metadata balance entry for account \"" + owner.String() + "\": " + + "invalid metadata address in denom \"" + denom + "\": " + err + " module=x/bank" + } + badChecksumErr := func(expected, actual string) string { + return "decoding bech32 failed: invalid checksum (expected " + expected + " got " + actual + ")" + } + nextKey := func(nextScopeID types.MetadataAddress) []byte { + // The prefix used for iteration is "nft/scope1". + // So the NextKey will be the bytes and checksum portion of the scope id as a bech32 string. + return []byte(strings.TrimPrefix(nextScopeID.String(), "scope1")) + } + + tests := []struct { + name string + balances []balance + valueOwner sdk.AccAddress + pageReq *query.PageRequest + expLinks types.AccMDLinks + expPageResp *query.PageResponse + expErr string + expLogs []string + }{ + { + name: "unpaginated: no scopes", + balances: []balance{ + {addr: addr1, denom: scopeID1.Denom()}, + {addr: addr3, denom: scopeID3.Denom()}, + }, + valueOwner: addr2, + expLinks: nil, + }, + { + name: "unpaginated: one scope", + balances: []balance{ + {addr: addr1, denom: scopeID1.Denom()}, + {addr: addr2, denom: scopeID2.Denom()}, + {addr: addr3, denom: scopeID3.Denom()}, + }, + valueOwner: addr2, + expLinks: types.AccMDLinks{{addr2, scopeID2}}, + }, + { + name: "unpaginated: three scopes", + balances: []balance{ + {addr: addr1, denom: scopeID1.Denom()}, + {addr: addr2, denom: scopeID2.Denom()}, + {addr: addr2, denom: scopeID3.Denom()}, + {addr: addr2, denom: scopeID4.Denom()}, + {addr: addr3, denom: scopeID5.Denom()}, + }, + valueOwner: addr2, + expLinks: types.AccMDLinks{{addr2, scopeID2}, {addr2, scopeID3}, {addr2, scopeID4}}, + }, + { + name: "unpaginated: four entries, three good, one bad", + balances: []balance{ + {addr: addr1, denom: scopeID1.Denom()}, + {addr: addr2, denom: scopeID2.Denom()}, + {addr: addr2, denom: scopeID3.Denom()}, + {addr: addr2, denom: scopeID3.Denom() + "x"}, + {addr: addr2, denom: scopeID4.Denom()}, + {addr: addr3, denom: scopeID5.Denom()}, + }, + valueOwner: addr2, + expLinks: types.AccMDLinks{{addr2, scopeID2}, {addr2, scopeID3}, {addr2, nil}, {addr2, scopeID4}}, + expLogs: []string{logMsg(addr2, scopeID3.Denom()+"x", badChecksumErr("t2w09p", "t2w0yx"))}, + }, + { + name: "unpaginated: four entries, all bad", + balances: []balance{ + {addr: addr1, denom: scopeID1.Denom() + "w"}, + {addr: addr1, denom: scopeID2.Denom() + "x"}, + {addr: addr1, denom: scopeID3.Denom() + "y"}, + {addr: addr1, denom: scopeID4.Denom() + "z"}, + }, + valueOwner: addr1, + expLinks: types.AccMDLinks{{addr1, nil}, {addr1, nil}, {addr1, nil}, {addr1, nil}}, + expLogs: []string{ + logMsg(addr1, scopeID2.Denom()+"x", badChecksumErr("2zk6kp", "2zk6hx")), + logMsg(addr1, scopeID3.Denom()+"y", badChecksumErr("t2w09p", "t2w0yy")), + logMsg(addr1, scopeID1.Denom()+"w", badChecksumErr("k95yrp", "k95yzw")), + logMsg(addr1, scopeID4.Denom()+"z", badChecksumErr("yn38up", "yn38az")), + }, + }, + { + name: "unpaginated: scope spec denom ignored", + balances: []balance{{addr: addr1, denom: scopeSpecID.Denom()}}, + valueOwner: addr1, + expLinks: nil, + }, + { + name: "unpaginated: scope spec denom ignored with scope results", + balances: []balance{ + {addr: addr1, denom: scopeSpecID.Denom()}, + {addr: addr1, denom: scopeID3.Denom()}, + {addr: addr1, denom: scopeID5.Denom()}, + }, + valueOwner: addr1, + expLinks: types.AccMDLinks{{addr1, scopeID3}, {addr1, scopeID5}}, + }, + { + name: "paginated: no scopes", + balances: []balance{ + {addr: addr1, denom: scopeID1.Denom()}, + {addr: addr3, denom: scopeID3.Denom()}, + }, + valueOwner: addr2, + pageReq: &query.PageRequest{Limit: 50}, + expLinks: nil, + expPageResp: &query.PageResponse{}, + }, + { + name: "paginated: one scope", + balances: []balance{ + {addr: addr1, denom: scopeID1.Denom()}, + {addr: addr2, denom: scopeID2.Denom()}, + {addr: addr3, denom: scopeID3.Denom()}, + }, + valueOwner: addr2, + pageReq: &query.PageRequest{Limit: 50, CountTotal: true}, + expLinks: types.AccMDLinks{{addr2, scopeID2}}, + expPageResp: &query.PageResponse{Total: 1}, + }, + { + name: "paginated: three scopes, get all", + balances: []balance{ + {addr: addr1, denom: scopeID1.Denom()}, + {addr: addr2, denom: scopeID2.Denom()}, + {addr: addr2, denom: scopeID3.Denom()}, + {addr: addr2, denom: scopeID4.Denom()}, + {addr: addr3, denom: scopeID5.Denom()}, + }, + valueOwner: addr2, + pageReq: &query.PageRequest{Limit: 3}, + expLinks: types.AccMDLinks{{addr2, scopeID2}, {addr2, scopeID3}, {addr2, scopeID4}}, + expPageResp: &query.PageResponse{}, + }, + { + name: "paginated: three scopes, get all reversed", + balances: []balance{ + {addr: addr3, denom: scopeID1.Denom()}, + {addr: addr1, denom: scopeID2.Denom()}, + {addr: addr1, denom: scopeID3.Denom()}, + {addr: addr1, denom: scopeID4.Denom()}, + {addr: addr2, denom: scopeID5.Denom()}, + }, + valueOwner: addr1, + pageReq: &query.PageRequest{Limit: 3, Reverse: true}, + expLinks: types.AccMDLinks{{addr1, scopeID4}, {addr1, scopeID3}, {addr1, scopeID2}}, + expPageResp: &query.PageResponse{}, + }, + { + name: "paginated: three scopes, get just first", + balances: []balance{ + {addr: addr2, denom: scopeID1.Denom()}, + {addr: addr3, denom: scopeID2.Denom()}, + {addr: addr3, denom: scopeID3.Denom()}, + {addr: addr3, denom: scopeID4.Denom()}, + {addr: addr1, denom: scopeID5.Denom()}, + }, + valueOwner: addr3, + pageReq: &query.PageRequest{Limit: 1}, + expLinks: types.AccMDLinks{{addr3, scopeID2}}, + expPageResp: &query.PageResponse{NextKey: nextKey(scopeID3)}, + }, + { + name: "paginated: three scopes, get just second using offset", + balances: []balance{ + {addr: addr1, denom: scopeID1.Denom()}, + {addr: addr3, denom: scopeID2.Denom()}, + {addr: addr3, denom: scopeID3.Denom()}, + {addr: addr3, denom: scopeID4.Denom()}, + {addr: addr2, denom: scopeID5.Denom()}, + }, + valueOwner: addr3, + pageReq: &query.PageRequest{Limit: 1, Offset: 1}, + expLinks: types.AccMDLinks{{addr3, scopeID3}}, + expPageResp: &query.PageResponse{NextKey: nextKey(scopeID4)}, + }, + { + name: "paginated: three scopes, get second using next key", + balances: []balance{ + {addr: addr3, denom: scopeID1.Denom()}, + {addr: addr2, denom: scopeID2.Denom()}, + {addr: addr2, denom: scopeID3.Denom()}, + {addr: addr2, denom: scopeID4.Denom()}, + {addr: addr1, denom: scopeID5.Denom()}, + }, + valueOwner: addr2, + pageReq: &query.PageRequest{Limit: 1, Key: nextKey(scopeID3)}, + expLinks: types.AccMDLinks{{addr2, scopeID3}}, + expPageResp: &query.PageResponse{NextKey: nextKey(scopeID4)}, + }, + { + name: "paginated: three scopes, get last by reversing with limit 1", + balances: []balance{ + {addr: addr2, denom: scopeID1.Denom()}, + {addr: addr1, denom: scopeID2.Denom()}, + {addr: addr1, denom: scopeID3.Denom()}, + {addr: addr1, denom: scopeID4.Denom()}, + {addr: addr3, denom: scopeID5.Denom()}, + }, + valueOwner: addr1, + pageReq: &query.PageRequest{Limit: 1, Reverse: true}, + expLinks: types.AccMDLinks{{addr1, scopeID4}}, + expPageResp: &query.PageResponse{NextKey: nextKey(scopeID3)}, + }, + { + name: "paginated: four entries, three good, one bad", + balances: []balance{ + {addr: addr1, denom: scopeID2.Denom()}, + {addr: addr1, denom: scopeID3.Denom() + "x"}, + {addr: addr1, denom: scopeID4.Denom()}, + {addr: addr1, denom: scopeID5.Denom()}, + }, + valueOwner: addr1, + pageReq: &query.PageRequest{Limit: 4}, + expLinks: types.AccMDLinks{{addr1, scopeID2}, {addr1, nil}, {addr1, scopeID4}, {addr1, scopeID5}}, + expPageResp: &query.PageResponse{}, + expLogs: []string{ + logMsg(addr1, scopeID3.Denom()+"x", badChecksumErr("t2w09p", "t2w0yx")), + }, + }, + { + name: "paginated: four entries, all bad", + balances: []balance{ + {addr: addr1, denom: scopeID2.Denom() + "w"}, + {addr: addr1, denom: scopeID3.Denom() + "x"}, + {addr: addr1, denom: scopeID4.Denom() + "y"}, + {addr: addr1, denom: scopeID5.Denom() + "z"}, + }, + valueOwner: addr1, + pageReq: &query.PageRequest{Limit: 4}, + expLinks: types.AccMDLinks{{addr1, nil}, {addr1, nil}, {addr1, nil}, {addr1, nil}}, + expPageResp: &query.PageResponse{}, + expLogs: []string{ + logMsg(addr1, scopeID2.Denom()+"w", badChecksumErr("2zk6kp", "2zk6hw")), + logMsg(addr1, scopeID3.Denom()+"x", badChecksumErr("t2w09p", "t2w0yx")), + logMsg(addr1, scopeID4.Denom()+"y", badChecksumErr("yn38up", "yn38ay")), + logMsg(addr1, scopeID5.Denom()+"z", badChecksumErr("9mfj0p", "9mfjwz")), + }, + }, + { + name: "paginated: four entries, all bad, get middle two", + balances: []balance{ + {addr: addr1, denom: scopeID2.Denom() + "w"}, + {addr: addr1, denom: scopeID3.Denom() + "x"}, + {addr: addr1, denom: scopeID4.Denom() + "y"}, + {addr: addr1, denom: scopeID5.Denom() + "z"}, + }, + valueOwner: addr1, + pageReq: &query.PageRequest{Limit: 2, Offset: 1, CountTotal: true}, + expLinks: types.AccMDLinks{{addr1, nil}, {addr1, nil}}, + expPageResp: &query.PageResponse{Total: 4, NextKey: append(nextKey(scopeID5), 'z')}, + expLogs: []string{ + logMsg(addr1, scopeID3.Denom()+"x", badChecksumErr("t2w09p", "t2w0yx")), + logMsg(addr1, scopeID4.Denom()+"y", badChecksumErr("yn38up", "yn38ay")), + }, + }, + { + name: "paginated: scope spec denom ignored", + balances: []balance{ + {addr: addr1, denom: scopeSpecID.Denom()}, + }, + valueOwner: addr1, + pageReq: &query.PageRequest{Limit: 50}, + expLinks: nil, + expPageResp: &query.PageResponse{}, + }, + { + name: "paginated: scope spec denom ignored with scope results", + balances: []balance{ + {addr: addr1, denom: scopeSpecID.Denom()}, + {addr: addr1, denom: scopeID2.Denom()}, + {addr: addr1, denom: scopeID4.Denom()}, + }, + valueOwner: addr1, + pageReq: &query.PageRequest{Limit: 50}, + expLinks: types.AccMDLinks{{addr1, scopeID2}, {addr1, scopeID4}}, + expPageResp: &query.PageResponse{}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + // Use a cache context for each test so that the setup doesn't persist between tests. + ctx, _ := s.ctx.CacheContext() + s.setBalances(ctx, tc.balances) + + callDesc := fmt.Sprintf("GetScopesForValueOwner(%q, %#v)", tc.valueOwner.String(), tc.pageReq) + s.logBuffer.Reset() + var links types.AccMDLinks + var pageResp *query.PageResponse + var err error + testFunc := func() { + links, pageResp, err = s.bk.GetScopesForValueOwner(ctx, tc.valueOwner, tc.pageReq) + } + s.Require().NotPanics(testFunc, callDesc) + logs := s.getLogOutput(callDesc) + + s.AssertErrorValue(err, tc.expErr, "error from %s", callDesc) + if !s.Assert().Equal(tc.expLinks, links, "links from %s", callDesc) { + expStrs := mapToStrings(tc.expLinks) + actStrs := mapToStrings(links) + s.Assert().Equal(expStrs, actStrs, "strings of the links") + } + + if !s.Assert().Equal(tc.expPageResp, pageResp, "page response from %s", callDesc) && tc.expPageResp != nil && pageResp != nil { + s.Assert().Equal(fmt.Sprintf("%q", string(tc.expPageResp.NextKey)), fmt.Sprintf("%q", string(pageResp.NextKey)), "quoted pageResp.NextKey") + s.Assert().Equal(int(tc.expPageResp.Total), int(pageResp.Total), "pageResp.Total as int") + } + + s.Assert().Equal(tc.expLogs, logs, "log output during %s", callDesc) + }) + } +} diff --git a/x/metadata/keeper/expected_keepers.go b/x/metadata/keeper/expected_keepers.go index a537a67b94..a188ded4ab 100644 --- a/x/metadata/keeper/expected_keepers.go +++ b/x/metadata/keeper/expected_keepers.go @@ -5,9 +5,11 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" "github.com/cosmos/cosmos-sdk/x/authz" markertypes "github.com/provenance-io/provenance/x/marker/types" + "github.com/provenance-io/provenance/x/metadata/types" ) // AuthKeeper is an interface with functions that the auth.Keeper has that are needed in this module. @@ -31,4 +33,17 @@ type AttrKeeper interface { // MarkerKeeper defines the attribute functionality needed by the metadata module. type MarkerKeeper interface { GetMarkerByDenom(ctx sdk.Context, denom string) (markertypes.MarkerAccountI, error) + IsMarkerAccount(ctx sdk.Context, addr sdk.AccAddress) bool +} + +type BankKeeper interface { + BlockedAddr(addr sdk.AccAddress) bool + MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + BurnCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error + + // These are methods not in the bank keeper, but that we add using our own MDBankKeeper. + + DenomOwner(ctx context.Context, denom string) (sdk.AccAddress, error) + GetScopesForValueOwner(ctx context.Context, valueOwner sdk.AccAddress, pageReq *query.PageRequest) (types.AccMDLinks, *query.PageResponse, error) } diff --git a/x/metadata/keeper/export_test.go b/x/metadata/keeper/export_test.go index c94489c50a..f08f3c614d 100644 --- a/x/metadata/keeper/export_test.go +++ b/x/metadata/keeper/export_test.go @@ -3,8 +3,6 @@ package keeper import ( storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/authz" - "github.com/provenance-io/provenance/x/metadata/types" ) @@ -32,96 +30,25 @@ func (k *Keeper) SetAuthzKeeper(authzKeeper AuthzKeeper) AuthzKeeper { return rv } -// TestablePartyDetails is the same as PartyDetails, but with -// public fields so that they can be created in unit tests as needed. -// Use the Real() method to convert it to a PartyDetails. -// I went this way instead of a NewTestPartyDetails constructor due to the -// number of arguments that one would need. Having named parameters (e.g. when -// defining a struct) is much easier to read and maintain. -type TestablePartyDetails struct { - Address string - Role types.PartyType - Optional bool - - Acc sdk.AccAddress - Signer string - SignerAcc sdk.AccAddress - - CanBeUsedBySpec bool - UsedBySpec bool -} - -// Real returns the PartyDetails version of this. -func (p TestablePartyDetails) Real() *PartyDetails { - return &PartyDetails{ - address: p.Address, - role: p.Role, - optional: p.Optional, - acc: p.Acc, - signer: p.Signer, - signerAcc: p.SignerAcc, - canBeUsedBySpec: p.CanBeUsedBySpec, - usedBySpec: p.UsedBySpec, - } -} - -// Testable is a TEST ONLY function that converts a PartyDetails into a TestablePartyDetails. -func (p *PartyDetails) Testable() TestablePartyDetails { - return TestablePartyDetails{ - Address: p.address, - Role: p.role, - Optional: p.optional, - Acc: p.acc, - Signer: p.signer, - SignerAcc: p.signerAcc, - CanBeUsedBySpec: p.canBeUsedBySpec, - UsedBySpec: p.usedBySpec, - } -} - -// Copy is a TEST ONLY function that copies a PartyDetails. -func (p *PartyDetails) Copy() *PartyDetails { - if p == nil { - return nil - } - rv := &PartyDetails{ - address: p.address, - role: p.role, - optional: p.optional, - acc: nil, - signer: p.signer, - signerAcc: nil, - canBeUsedBySpec: p.canBeUsedBySpec, - usedBySpec: p.usedBySpec, - } - if p.acc != nil { - rv.acc = make(sdk.AccAddress, len(p.acc)) - copy(rv.acc, p.acc) - } - if p.signerAcc != nil { - rv.signerAcc = make(sdk.AccAddress, len(p.signerAcc)) - copy(rv.signerAcc, p.signerAcc) - } +// SetBankKeeper is a TEST ONLY setter for the keeper's bank keeper. +// It returns the previously defined BankKeeper +func (k *Keeper) SetBankKeeper(bankKeeper BankKeeper) BankKeeper { + rv := k.bankKeeper + k.bankKeeper = bankKeeper return rv } -var ( - // AuthzCacheAcceptableKey is a TEST ONLY exposure of authzCacheAcceptableKey. - AuthzCacheAcceptableKey = authzCacheAcceptableKey - // AuthzCacheIsWasmKey is a TEST ONLY exposure of authzCacheIsWasmKey. - AuthzCacheIsWasmKey = authzCacheIsWasmKey - // AuthzCacheContextKey is a TEST ONLY exposure of authzCacheContextKey. - AuthzCacheContextKey = authzCacheContextKey -) - -// AcceptableMap is a TEST ONLY exposure of the AuthzCache.acceptable map. -func (c *AuthzCache) AcceptableMap() map[string]authz.Authorization { - return c.acceptable +// SetBankKeeper is a TEST ONLY setter for the keeper's marker keeper. +// It returns the previously defined MarkerKeeper +func (k *Keeper) SetMarkerKeeper(markerKeeper MarkerKeeper) MarkerKeeper { + rv := k.markerKeeper + k.markerKeeper = markerKeeper + return rv } -// IsWasmMap is a TEST ONLY exposure of the AuthzCache.isWasm map. -func (c *AuthzCache) IsWasmMap() map[string]bool { - return c.isWasm +// WriteScopeToState is a TEST ONLY exposure of writeScopeToState. +func (k *Keeper) WriteScopeToState(ctx sdk.Context, scope types.Scope) { + k.writeScopeToState(ctx, scope) } // ValidateAllRequiredPartiesSigned is a TEST ONLY exposure of validateAllRequiredPartiesSigned. @@ -130,7 +57,7 @@ func (k Keeper) ValidateAllRequiredPartiesSigned( reqParties, availableParties []types.Party, reqRoles []types.PartyType, msg types.MetadataMsg, -) ([]*PartyDetails, error) { +) ([]*types.PartyDetails, error) { return k.validateAllRequiredPartiesSigned(ctx, reqParties, availableParties, reqRoles, msg) } @@ -160,10 +87,10 @@ func (k Keeper) FindAuthzGrantee( // AssociateAuthorizations is a TEST ONLY exposure of associateAuthorizations. func (k Keeper) AssociateAuthorizations( ctx sdk.Context, - parties []*PartyDetails, + parties []*types.PartyDetails, signers *SignersWrapper, msg types.MetadataMsg, - onAssociation func(party *PartyDetails) (stop bool), + onAssociation func(party *types.PartyDetails) (stop bool), ) error { return k.associateAuthorizations(ctx, parties, signers, msg, onAssociation) } @@ -172,7 +99,7 @@ func (k Keeper) AssociateAuthorizations( func (k Keeper) AssociateAuthorizationsForRoles( ctx sdk.Context, roles []types.PartyType, - parties []*PartyDetails, + parties []*types.PartyDetails, signers *SignersWrapper, msg types.MetadataMsg, ) (bool, error) { @@ -180,7 +107,7 @@ func (k Keeper) AssociateAuthorizationsForRoles( } // ValidateProvenanceRole is a TEST ONLY exposure of validateProvenanceRole. -func (k Keeper) ValidateProvenanceRole(ctx sdk.Context, parties []*PartyDetails) error { +func (k Keeper) ValidateProvenanceRole(ctx sdk.Context, parties []*types.PartyDetails) error { return k.validateProvenanceRole(ctx, parties) } @@ -190,53 +117,37 @@ func (k Keeper) IsWasmAccount(ctx sdk.Context, addr sdk.AccAddress) bool { } // ValidateAllRequiredSigned is a TEST ONLY exposure of validateAllRequiredSigned. -func (k Keeper) ValidateAllRequiredSigned(ctx sdk.Context, required []string, msg types.MetadataMsg) ([]*PartyDetails, error) { +func (k Keeper) ValidateAllRequiredSigned(ctx sdk.Context, required []string, msg types.MetadataMsg) ([]*types.PartyDetails, error) { return k.validateAllRequiredSigned(ctx, required, msg) } // ValidateSmartContractSigners is a TEST ONLY exposure of validateSmartContractSigners. -func (k Keeper) ValidateSmartContractSigners(ctx sdk.Context, usedSigners UsedSignersMap, msg types.MetadataMsg) error { +func (k Keeper) ValidateSmartContractSigners(ctx sdk.Context, usedSigners types.UsedSignersMap, msg types.MetadataMsg) error { return k.validateSmartContractSigners(ctx, usedSigners, msg) } -// ValidateScopeValueOwnerChangeFromExisting is a TEST ONLY exposure of validateScopeValueOwnerChangeFromExisting. -func (k Keeper) ValidateScopeValueOwnerChangeFromExisting( - ctx sdk.Context, - existing string, - signers *SignersWrapper, - msg types.MetadataMsg, -) (UsedSignersMap, error) { - return k.validateScopeValueOwnerChangeFromExisting(ctx, existing, signers, msg) -} - -// ValidateScopeValueOwnerChangeToProposed is a TEST ONLY exposure of validateScopeValueOwnerChangeToProposed. -func (k Keeper) ValidateScopeValueOwnerChangeToProposed( - ctx sdk.Context, - proposed string, - signers *SignersWrapper, -) (UsedSignersMap, error) { - return k.validateScopeValueOwnerChangeToProposed(ctx, proposed, signers) -} - var ( // ValidateRolesPresent is a TEST ONLY exposure of validateRolesPresent. ValidateRolesPresent = validateRolesPresent // ValidatePartiesArePresent is a TEST ONLY exposure of validatePartiesArePresent. ValidatePartiesArePresent = validatePartiesArePresent - // FindMissing is a TEST ONLY exposure of findMissing. - FindMissing = findMissing - // FindMissingParties is a TEST ONLY exposure of findMissingParties. - FindMissingParties = findMissingParties ) -// FindMissingComp is a TEST ONLY exposure of findMissingComp. -func FindMissingComp[R any, C any](required []R, toCheck []C, comp func(R, C) bool) []R { - return findMissingComp(required, toCheck, comp) -} - var ( - // PluralEnding is a TEST ONLY exposure of pluralEnding. - PluralEnding = pluralEnding // SafeBech32ToAccAddresses is a TEST ONLY exposure of safeBech32ToAccAddresses. SafeBech32ToAccAddresses = safeBech32ToAccAddresses ) + +var ( + // NewKeeper3To4 is a TEST ONLY exposure of newKeeper3To4. + NewKeeper3To4 = newKeeper3To4 + // MigrateValueOwners is a TEST ONLY exposure of migrateValueOwners. + MigrateValueOwners = migrateValueOwners + // MigrateValueOwnerToBank is a TEST ONLY exposure of migrateValueOwnerToBank. + MigrateValueOwnerToBank = migrateValueOwnerToBank + // DeleteValueOwnerIndexEntries is a TEST ONLY exposure of deleteValueOwnerIndexEntries. + DeleteValueOwnerIndexEntries = deleteValueOwnerIndexEntries +) + +// Keeper3To4 is a TEST ONLY exposure of keeper3To4. +type Keeper3To4 = keeper3To4 diff --git a/x/metadata/keeper/genesis.go b/x/metadata/keeper/genesis.go index 1fb9c2a0cb..6e0cb0d6f4 100644 --- a/x/metadata/keeper/genesis.go +++ b/x/metadata/keeper/genesis.go @@ -16,7 +16,9 @@ func (k Keeper) InitGenesis(ctx sdk.Context, data *types.GenesisState) { } if data.Scopes != nil { for _, s := range data.Scopes { - k.SetScope(ctx, s) + if err := k.SetScope(ctx, s); err != nil { + panic(err) + } } } if data.Sessions != nil { diff --git a/x/metadata/keeper/keeper.go b/x/metadata/keeper/keeper.go index f29470a087..e438d25c7b 100644 --- a/x/metadata/keeper/keeper.go +++ b/x/metadata/keeper/keeper.go @@ -8,107 +8,21 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" "github.com/cosmos/gogoproto/proto" "github.com/provenance-io/provenance/x/metadata/types" ) -// MetadataKeeperI is the internal state api for the metadata module. -type MetadataKeeperI interface { - // GetScope returns the scope with the given id. - GetScope(sdk.Context, types.MetadataAddress) (types.Scope, bool) - // SetScope stores a scope in the module kv store. - SetScope(sdk.Context, types.Scope) - // RemoveScope removes a scope from the module kv store along with all its records and sessions. - RemoveScope(sdk.Context, types.MetadataAddress) - - // IterateScopes processes all stored scopes with the given handler. - IterateScopes(sdk.Context, func(types.Scope) bool) error - // IterateScopesForAddress processes scopes associated with the provided address with the given handler. - IterateScopesForAddress(sdk.Context, sdk.AccAddress, func(types.MetadataAddress) bool) error - // IterateScopesForScopeSpec processes scopes associated with the provided scope specification id with the given handler. - IterateScopesForScopeSpec(sdk.Context, types.MetadataAddress, func(types.MetadataAddress) bool) error - - // GetSession returns the session with the given id. - GetSession(sdk.Context, types.MetadataAddress) (types.Session, bool) - // SetSession stores a session in the module kv store. - SetSession(sdk.Context, types.Session) - // RemoveSession removes a session from the module kv store if there are no records associated with it. - RemoveSession(sdk.Context, types.MetadataAddress) - - // IterateSessions processes stored sessions with the given handler. - IterateSessions(sdk.Context, types.MetadataAddress, func(types.Session) bool) error - - // GetRecord returns the record with the given id. - GetRecord(sdk.Context, types.MetadataAddress) (types.Record, bool) - // GetRecords returns records for a scope optionally limited to a name. - GetRecords(sdk.Context, types.MetadataAddress, string) ([]*types.Record, error) - // SetRecord stores a record in the module kv store. - SetRecord(sdk.Context, types.Record) - // RemoveRecord removes a record from the module kv store. - RemoveRecord(sdk.Context, types.MetadataAddress) - - // IterateRecords processes stored records with the given handler. - IterateRecords(sdk.Context, types.MetadataAddress, func(types.Record) bool) error - - // GetScopeSpecification returns the scope specification with the given id. - GetScopeSpecification(sdk.Context, types.MetadataAddress) (types.ScopeSpecification, bool) - // SetScopeSpecification stores a scope specification in the module kv store. - SetScopeSpecification(sdk.Context, types.ScopeSpecification) - // RemoveScopeSpecification removes a scope specification from the module kv store. - RemoveScopeSpecification(sdk.Context, types.MetadataAddress) error - - // IterateScopeSpecs processes all scope specs using a given handler. - IterateScopeSpecs(ctx sdk.Context, handler func(specification types.ScopeSpecification) (stop bool)) error - // IterateScopeSpecsForOwner processes all scope specs owned by an address using a given handler. - IterateScopeSpecsForOwner(ctx sdk.Context, ownerAddress sdk.AccAddress, handler func(scopeSpecID types.MetadataAddress) (stop bool)) error - // IterateScopeSpecsForContractSpec processes all scope specs associated with a contract spec id using a given handler. - IterateScopeSpecsForContractSpec(ctx sdk.Context, contractSpecID types.MetadataAddress, handler func(scopeSpecID types.MetadataAddress) (stop bool)) error - - // GetContractSpecification returns the contract specification with the given id. - GetContractSpecification(sdk.Context, types.MetadataAddress) (types.ContractSpecification, bool) - // SetContractSpecification stores a contract specification in the module kv store. - SetContractSpecification(sdk.Context, types.ContractSpecification) - // RemoveContractSpecification removes a contract specification from the module kv store. - RemoveContractSpecification(sdk.Context, types.MetadataAddress) error - - // IterateContractSpecs processes all contract specs using a given handler. - IterateContractSpecs(ctx sdk.Context, handler func(specification types.ContractSpecification) (stop bool)) error - // IterateContractSpecsForOwner processes all contract specs owned by an address using a given handler. - IterateContractSpecsForOwner(ctx sdk.Context, ownerAddress sdk.AccAddress, handler func(contractSpecID types.MetadataAddress) (stop bool)) error - - // GetRecordSpecification returns the record specification with the given id. - GetRecordSpecification(sdk.Context, types.MetadataAddress) (types.RecordSpecification, bool) - // SetRecordSpecification stores a record specification in the module kv store. - SetRecordSpecification(sdk.Context, types.RecordSpecification) - // RemoveRecordSpecification removes a record specification from the module kv store. - RemoveRecordSpecification(sdk.Context, types.MetadataAddress) error - - // IterateRecordSpecs processes all record specs using a given handler. - IterateRecordSpecs(ctx sdk.Context, handler func(specification types.RecordSpecification) (stop bool)) error - // IterateRecordSpecsForOwner processes all record specs owned by an address using a given handler. - IterateRecordSpecsForOwner(ctx sdk.Context, ownerAddress sdk.AccAddress, handler func(recordSpecID types.MetadataAddress) (stop bool)) error - // IterateRecordSpecsForContractSpec processes all record specs for a contract spec using a given handler. - IterateRecordSpecsForContractSpec(ctx sdk.Context, contractSpecID types.MetadataAddress, handler func(recordSpecID types.MetadataAddress) (stop bool)) error - // GetRecordSpecificationsForContractSpecificationID returns all the record specifications associated with given contractSpecID - GetRecordSpecificationsForContractSpecificationID(ctx sdk.Context, contractSpecID types.MetadataAddress) ([]*types.RecordSpecification, error) - - // GetOsLocatorRecord returns the OS locator records for a given name record. - GetOsLocatorRecord(ctx sdk.Context, ownerAddr sdk.AccAddress) (types.ObjectStoreLocator, bool) - // return if OSLocator exists for a given owner addr - OSLocatorExists(ctx sdk.Context, ownerAddr sdk.AccAddress) bool - // add OSLocator instance - SetOSLocator(ctx sdk.Context, ownerAddr, encryptionKey sdk.AccAddress, uri string) error - // get OS locator by scope UUID. - GetOSLocatorByScope(ctx sdk.Context, scopeID string) ([]types.ObjectStoreLocator, error) -} - // Keeper is the concrete state-based API for the metadata module. type Keeper struct { // Key to access the key-value store from sdk.Context storeKey storetypes.StoreKey cdc codec.BinaryCodec + moduleAddr sdk.AccAddress + // To check if accounts exist and set public keys. authKeeper AuthKeeper @@ -120,20 +34,26 @@ type Keeper struct { // For getting marker accounts markerKeeper MarkerKeeper + + // For managing value owners + bankKeeper BankKeeper } // NewKeeper creates new instances of the metadata Keeper. func NewKeeper( cdc codec.BinaryCodec, key storetypes.StoreKey, authKeeper AuthKeeper, authzKeeper AuthzKeeper, attrKeeper AttrKeeper, markerKeeper MarkerKeeper, + bankKeeper bankkeeper.BaseKeeper, ) Keeper { return Keeper{ storeKey: key, cdc: cdc, + moduleAddr: authtypes.NewModuleAddress(types.ModuleName), authKeeper: authKeeper, authzKeeper: authzKeeper, attrKeeper: attrKeeper, markerKeeper: markerKeeper, + bankKeeper: NewMDBankKeeper(bankKeeper), } } @@ -142,8 +62,6 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/"+types.ModuleName) } -var _ MetadataKeeperI = &Keeper{} - // VerifyCorrectOwner to determines whether the signer resolves to the owner of the OSLocator record. func (k Keeper) VerifyCorrectOwner(ctx sdk.Context, ownerAddr sdk.AccAddress) bool { stored, found := k.GetOsLocatorRecord(ctx, ownerAddr) diff --git a/x/metadata/keeper/keeper_test.go b/x/metadata/keeper/keeper_test.go index e73f9c3cd3..2fc66d2aff 100644 --- a/x/metadata/keeper/keeper_test.go +++ b/x/metadata/keeper/keeper_test.go @@ -112,6 +112,23 @@ func ownerPartyList(addresses ...string) []types.Party { return retval } +// addrsToStrings returns the bech32 address of each of the provided addrs. +func addrsToStrings(addrs []sdk.AccAddress) []string { + if addrs == nil { + return nil + } + rv := make([]string, len(addrs)) + for i, v := range addrs { + rv[i] = v.String() + } + return rv +} + +// FreshCtx returns a new sdk.Context with a types.AuthzCache in it. +func FreshCtx(app *simapp.App) sdk.Context { + return types.AddAuthzCacheToContext(app.NewContext(false)) +} + func (s *KeeperTestSuite) TestParams() { s.T().Run("os param tests", func(t *testing.T) { osp := s.app.MetadataKeeper.GetOSLocatorParams(s.ctx) @@ -214,7 +231,6 @@ func (s *KeeperTestSuite) TestDeleteOSLocator() { r, found := s.app.MetadataKeeper.GetOsLocatorRecord(s.ctx, s.user1Addr) s.Require().Empty(r) s.Require().False(found) - }) } diff --git a/x/metadata/keeper/migrations_v4.go b/x/metadata/keeper/migrations_v4.go new file mode 100644 index 0000000000..a9b89ec25e --- /dev/null +++ b/x/metadata/keeper/migrations_v4.go @@ -0,0 +1,169 @@ +package keeper + +import ( + "fmt" + + "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/cosmos/gogoproto/proto" + + markertypes "github.com/provenance-io/provenance/x/marker/types" + "github.com/provenance-io/provenance/x/metadata/types" +) + +// Migrate3To4 will update the metadata store from version 3 to version 4. This should be part of the viridian upgrade. +func (m Migrator) Migrate3To4(ctx sdk.Context) error { + logger := m.keeper.Logger(ctx) + logger.Info("Starting migration of x/metadata from 3 to 4.") + if err := migrateValueOwners(ctx, newKeeper3To4(m.keeper)); err != nil { + logger.Error("Error migrating scope value owners.", "error", err) + return err + } + logger.Info("Done migrating x/metadata from 3 to 4.") + return nil +} + +// keeper3To4I is an interface with keeper-related stuff needed to migrate metadata from 3 to 4. +type keeper3To4I interface { + Logger(ctx sdk.Context) log.Logger + GetStore(ctx sdk.Context) storetypes.KVStore + Unmarshal(bz []byte, ptr proto.Message) error + SetScopeValueOwner(ctx sdk.Context, scopeID types.MetadataAddress, newValueOwner string) error +} + +// keeper3To4 is a wrapper on the metadata Keeper with a few extra things exposed to satisfy the keeper3To4I interface. +type keeper3To4 struct { + Keeper +} + +// newKeeper3To4 wraps the provided metadata Keeper as a keeper3To4. +func newKeeper3To4(kpr Keeper) keeper3To4 { + return keeper3To4{Keeper: kpr} +} + +// GetStore returns a store for the metadata stuff. +func (k keeper3To4) GetStore(ctx sdk.Context) storetypes.KVStore { + return ctx.KVStore(k.Keeper.storeKey) +} + +// Unmarshal uses the Keeper's codec to unmarshal something. +func (k keeper3To4) Unmarshal(bz []byte, ptr proto.Message) error { + return k.Keeper.cdc.Unmarshal(bz, ptr) +} + +// V3WriteNewScope writes a new scope to state in the way v3 of the metadata module did. +// Deprecated: Only exists to facilitate testing of the migration of the metadata module from v3 to v4. +func (k Keeper) V3WriteNewScope(ctx sdk.Context, scope types.Scope) error { + store := ctx.KVStore(k.storeKey) + if store.Has(scope.ScopeId) { + return fmt.Errorf("scope %s already exists", scope.ScopeId) + } + bz, err := k.cdc.Marshal(&scope) + if err != nil { + return fmt.Errorf("could not marshal scope %s: %w", scope.ScopeId, err) + } + store.Set(scope.ScopeId, bz) + k.indexScope(store, &scope, nil) + // indexScope no longer does anything with the value owner address, but + // we used to have a couple index entries for it. + if len(scope.ValueOwnerAddress) > 0 { + vo := sdk.MustAccAddressFromBech32(scope.ValueOwnerAddress) + k1 := types.GetAddressScopeCacheKey(vo, scope.ScopeId) + k2 := GetValueOwnerScopeCacheKey(vo, scope.ScopeId) + store.Set(k1, []byte{0x01}) + store.Set(k2, []byte{0x01}) + } + return nil +} + +// migrateValueOwners will loop through all scopes and move the value owner info into the bank module. +func migrateValueOwners(ctx sdk.Context, kpr keeper3To4I) error { + logger := kpr.Logger(ctx) + logger.Info("Moving scope value owner data into x/bank ledger.") + store := kpr.GetStore(ctx) + it := storetypes.KVStorePrefixIterator(store, types.ScopeKeyPrefix) + defer it.Close() + + // If a scope's value owner is a marker, someone had the required deposit permission. + // But we don't have that permission here, and have no way to get it again. So, we + // bypass the marker send restrictions under the assumption that if a scope has a + // marker for a value owner, it was set that way by someone with proper permissions. + // We do NOT bypass the quarantine send restrictions though because we don't + // actually know that the value owner wanted to be the value owner of the scope. + ctx = markertypes.WithBypass(ctx) + + scopeCount := 0 + valueOwnerCount := 0 + for ; it.Valid(); it.Next() { + scopeCount++ + scopeBz := it.Value() + var scope types.Scope + if err := kpr.Unmarshal(scopeBz, &scope); err != nil { + scopeID := types.MetadataAddress(it.Key()) + logger.Error(fmt.Sprintf("[%d]: ScopeID=%q", scopeCount, scopeID), "bytes", scopeBz) + return fmt.Errorf("error reading scope %s from state: %w", scopeID, err) + } + + if len(scope.ValueOwnerAddress) > 0 { + valueOwnerCount++ + if err := migrateValueOwnerToBank(ctx, kpr, store, scope); err != nil { + return err + } + } + + if scopeCount%10_000 == 0 { + logger.Info("Progress update:", "scopes", scopeCount, "value owners", valueOwnerCount) + } + } + logger.Info("Done moving scope value owners into bank module.", "scopes", scopeCount, "value owners", valueOwnerCount) + return nil +} + +// migrateValueOwnerToBank will switch a scope's value owner to be maintained by the bank module instead of in the scope. +func migrateValueOwnerToBank(ctx sdk.Context, kpr keeper3To4I, store storetypes.KVStore, scope types.Scope) error { + if err := kpr.SetScopeValueOwner(ctx, scope.ScopeId, scope.ValueOwnerAddress); err != nil { + return fmt.Errorf("could not migrate scope %s value owner %q to bank module: %w", + scope.ScopeId, scope.ValueOwnerAddress, err) + } + deleteValueOwnerIndexEntries(store, scope) + return nil +} + +// valueOwnerScopeCacheKeyPrefix is the prefix key that we used to use for a value owner -> scope index. +var valueOwnerScopeCacheKeyPrefix = []byte{0x18} + +// getValueOwnerScopeCacheIteratorPrefix returns an iterator prefix for all scope cache entries assigned to a given address +func getValueOwnerScopeCacheIteratorPrefix(addr sdk.AccAddress) []byte { + return append(valueOwnerScopeCacheKeyPrefix, address.MustLengthPrefix(addr.Bytes())...) +} + +// GetValueOwnerScopeCacheKey returns the store key for an address cache entry +func GetValueOwnerScopeCacheKey(addr sdk.AccAddress, scopeID types.MetadataAddress) []byte { + return append(getValueOwnerScopeCacheIteratorPrefix(addr), scopeID.Bytes()...) +} + +// deleteValueOwnerIndexEntries will delete the index entries involving a scope's value owner. +func deleteValueOwnerIndexEntries(store storetypes.KVStore, scope types.Scope) { + // Don't do anything without a valid value owner. + vo, err := sdk.AccAddressFromBech32(scope.ValueOwnerAddress) + if err != nil || len(vo) == 0 { + return + } + + // Delete the value owner -> scope index entry (that's now a denom owners thing). + key := GetValueOwnerScopeCacheKey(vo, scope.ScopeId) + store.Delete(key) + + // The address -> scope index no longer associates a value owner with a scope; it only applies to the Owners. + // So, if the value owner is also in the list of owners, we keep the entry, otherwise, we delete it. + for _, owner := range scope.Owners { + if owner.Address == scope.ValueOwnerAddress { + return // The value owner is also an owner. Nothing more to do. + } + } + key = types.GetAddressScopeCacheKey(vo, scope.ScopeId) + store.Delete(key) +} diff --git a/x/metadata/keeper/migrations_v4_test.go b/x/metadata/keeper/migrations_v4_test.go new file mode 100644 index 0000000000..b45351a067 --- /dev/null +++ b/x/metadata/keeper/migrations_v4_test.go @@ -0,0 +1,641 @@ +package keeper_test + +import ( + "bytes" + "errors" + "fmt" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "cosmossdk.io/log" + + storetypes "cosmossdk.io/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" + + simapp "github.com/provenance-io/provenance/app" + "github.com/provenance-io/provenance/internal" + "github.com/provenance-io/provenance/internal/provutils" + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/testutil/testlog" + "github.com/provenance-io/provenance/x/metadata/keeper" + "github.com/provenance-io/provenance/x/metadata/types" +) + +type wrappedKVStore struct { + storetypes.KVStore + calls *storeCalls +} + +func (s *wrappedKVStore) Delete(key []byte) { + s.calls.Deletions = append(s.calls.Deletions, key) + if s.KVStore != nil { + s.KVStore.Delete(key) + } +} + +type storeCalls struct { + Deletions [][]byte +} + +type testKeeper3To4 struct { + keeper.Keeper3To4 + + logBuffer bytes.Buffer + + storeCalls *storeCalls + + unmarshalErrs []string + + setScopeValueOwnerErrs []string + setScopeValueOwnersCalls []*provutils.Pair[types.MetadataAddress, string] +} + +func newTestKeeper3To4(kpr keeper.Keeper) *testKeeper3To4 { + return &testKeeper3To4{ + Keeper3To4: keeper.NewKeeper3To4(kpr), + storeCalls: &storeCalls{}, + } +} + +func (k *testKeeper3To4) Logger(_ sdk.Context) log.Logger { + return internal.NewBufferedInfoLogger(&k.logBuffer) +} + +func (k *testKeeper3To4) GetStore(ctx sdk.Context) storetypes.KVStore { + store := k.Keeper3To4.GetStore(ctx) + return &wrappedKVStore{ + KVStore: store, + calls: k.storeCalls, + } +} + +func (k *testKeeper3To4) Unmarshal(bz []byte, ptr proto.Message) error { + if len(k.unmarshalErrs) > 0 { + rv := k.unmarshalErrs[0] + k.unmarshalErrs = k.unmarshalErrs[1:] + if len(rv) > 0 { + return errors.New(rv) + } + } + return k.Keeper3To4.Unmarshal(bz, ptr) +} + +func (k *testKeeper3To4) SetScopeValueOwner(ctx sdk.Context, scopeID types.MetadataAddress, newValueOwner string) error { + k.setScopeValueOwnersCalls = append(k.setScopeValueOwnersCalls, provutils.NewPair(scopeID, newValueOwner)) + if len(k.setScopeValueOwnerErrs) > 0 { + rv := k.setScopeValueOwnerErrs[0] + k.setScopeValueOwnerErrs = k.setScopeValueOwnerErrs[1:] + if len(rv) > 0 { + return errors.New(rv) + } + } + return nil +} + +// GetLogOutput gets the log buffer contents. This (probably) also clears the log buffer. +func (k *testKeeper3To4) GetLogOutput(t *testing.T, msg string, args ...interface{}) []string { + return getLogOutput(t, k.logBuffer, msg, args...) +} + +// getLogOutput gets the log buffer contents. This (probably) also clears the log buffer. +func getLogOutput(t *testing.T, logBuffer bytes.Buffer, msg string, args ...interface{}) []string { + logOutput := logBuffer.String() + t.Logf(msg+" log output:\n%s", append(args, logOutput)) + return internal.SplitLogLines(logOutput) +} + +func writeScope(t *testing.T, kpr keeper.Keeper, ctx sdk.Context, scope types.Scope, msgAndArgs ...interface{}) { + if len(msgAndArgs) == 0 { + msgAndArgs = append(msgAndArgs, "V3WriteNewScope") + } else { + switch v := msgAndArgs[0].(type) { + case string: + msgAndArgs[0] = "V3WriteNewScope: " + v + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + if len(msgAndArgs) == 1 { + msgAndArgs = []interface{}{"[%d]: V3WriteNewScope", v} + } + } + } + err := kpr.V3WriteNewScope(ctx, scope) + require.NoError(t, err, msgAndArgs...) +} + +func TestMigrate3to4(t *testing.T) { + addrs := []string{ + newAddr("one").String(), // cosmos1dahx2h6lta047h6lta047h6lta047h6lq2tdll + newAddr("two").String(), // cosmos1w3mk7h6lta047h6lta047h6lta047h6lakjg9t + newAddr("three").String(), // cosmos1w358yet9ta047h6lta047h6lta047h6lma20rt + newAddr("four").String(), // cosmos1vehh2ujlta047h6lta047h6lta047h6l6dna47 + newAddr("five").String(), // cosmos1ve5hve2lta047h6lta047h6lta047h6ltfdqga + newAddr("six").String(), // cosmos1wd5hsh6lta047h6lta047h6lta047h6la8xq2y + newAddr("seven").String(), // cosmos1wdjhvetwta047h6lta047h6lta047h6ldw3pw9 + newAddr("eight").String(), // cosmos1v45kw6r5ta047h6lta047h6lta047h6l3j2t8s + newAddr("nine").String(), // cosmos1de5kue2lta047h6lta047h6lta047h6lfr0cjd + newAddr("ten").String(), // cosmos1w3jkuh6lta047h6lta047h6lta047h6lqcnhxg + } + + newUUID := func(i int) uuid.UUID { + // Sixteen 9's is the largest number we can handle; one more and it's 17 digits. + require.LessOrEqual(t, i, 9999999999999999, "value provided to newScopeID") + str := fmt.Sprintf("________________%d", i) + str = str[len(str)-16:] + rv, err := uuid.FromBytes([]byte(str)) + require.NoError(t, err, "uuid.FromBytes([]byte(%q))", str) + return rv + } + newScopeID := func(i int) types.MetadataAddress { + return types.ScopeMetadataAddress(newUUID(i)) + } + newSpecID := func(i int) types.MetadataAddress { + // The spec id shouldn't really matter in here, but I want it different from a scope's i. + // So I do some math to make it seem kind of random, but is still deterministic. + // 48, 67, and 81 were picked randomly and have no special meaning. + // 50,000 was chosen so that maybe some spec ids get used more than once. + j := (i + 48) * (i + 67) * (i + 81) + return types.ScopeSpecMetadataAddress(newUUID(j % 50_000)) + } + newScope := func(i int) types.Scope { + rv := types.Scope{ + ScopeId: newScopeID(i), + SpecificationId: newSpecID(i), + ValueOwnerAddress: addrs[i%len(addrs)], + } + if i%7 == 0 { + rv.ValueOwnerAddress = "" + } + ownerCount := (i % 3) + 1 // 1 to 3. + if ownerCount > 0 { + rv.Owners = make([]types.Party, ownerCount) + for o := range rv.Owners { + rv.Owners[o].Address = addrs[(i*i+o)%len(addrs)] + rv.Owners[o].Role = types.PartyType(1 + (i+o)%11) // 11 different roles, 1 to 11. + } + } + return rv + } + + var logBuffer bytes.Buffer + app := func() *simapp.App { + // Swap in a logger maker that writes to our logBuffer, and defer a call to set it back when we're done. + defer simapp.SetLoggerMaker(simapp.SetLoggerMaker(simapp.BufferedInfoLoggerMaker(&logBuffer))) + return simapp.Setup(t) + }() + ctx1 := FreshCtx(app) + for i := 1; i <= 100; i++ { + j := i * i * i * i * i * i * i // i^7. When i = 100, j = 100,000,000,000,000 = 15 digits. + if i%2 == 0 { + j = 1_000_000_000_000_000 - j + } + writeScope(t, app.MetadataKeeper, ctx1, newScope(i), i) + } + + tests := []struct { + name string + setup func(t *testing.T, ctx sdk.Context) + expErr string + expLogs []string + }{ + { + name: "error from one", + setup: func(t *testing.T, ctx sdk.Context) { + key := newScopeID(5000000000000000) + value := []byte{0, 0, 0} + store := ctx.KVStore(app.MetadataKeeper.GetStoreKey()) + store.Set(key, value) + }, + expErr: "error reading scope " + newScopeID(5000000000000000).String() + " from state: proto: Scope: illegal tag 0 (wire type 0)", + expLogs: []string{ + "INF Starting migration of x/metadata from 3 to 4. module=x/metadata", + "INF Moving scope value owner data into x/bank ledger. module=x/metadata", + "ERR [1]: ScopeID=\"" + newScopeID(5000000000000000).String() + "\" bytes=\"\\x00\\x00\\x00\" module=x/metadata", + "ERR Error migrating scope value owners. error=\"error reading scope " + + newScopeID(5000000000000000).String() + " from state: proto: Scope: illegal tag 0 (wire type 0)\" " + + "module=x/metadata", + }, + }, + { + name: "all good", + expLogs: []string{ + "INF Starting migration of x/metadata from 3 to 4. module=x/metadata", + "INF Moving scope value owner data into x/bank ledger. module=x/metadata", + "INF Done moving scope value owners into bank module. module=x/metadata scopes=100 value owners=86", + "INF Done migrating x/metadata from 3 to 4. module=x/metadata", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx, _ := FreshCtx(app).CacheContext() + if tc.setup != nil { + tc.setup(t, ctx) + } + + logBuffer.Reset() + migrator := keeper.NewMigrator(app.MetadataKeeper) + var err error + testFunc := func() { + err = migrator.Migrate3To4(ctx) + } + require.NotPanics(t, testFunc, "Migrate3To4") + assertions.AssertErrorValue(t, err, tc.expErr, "error from Migrate3To4") + actLogs := getLogOutput(t, logBuffer, "Migrate3To4") + assert.Equal(t, tc.expLogs, actLogs, "logs messages emitted during Migrate3To4") + }) + } +} + +func TestMigrateValueOwners(t *testing.T) { + newUUID := func(i int) uuid.UUID { + // Sixteen 9's is the largest number we can handle; one more and it's 17 digits. + require.LessOrEqual(t, i, 9999999999999999, "value provided to newScopeID") + str := fmt.Sprintf("________________%d", i) + str = str[len(str)-16:] + rv, err := uuid.FromBytes([]byte(str)) + require.NoError(t, err, "uuid.FromBytes([]byte(%q))", str) + return rv + } + newScopeID := func(i int) types.MetadataAddress { + return types.ScopeMetadataAddress(newUUID(i)) + } + newSpecID := func(i int) types.MetadataAddress { + // The spec id shouldn't really matter in here, but I want it different from a scope's i. + // So I do some math to make it seem kind of random, but is still deterministic. + // 48, 67, and 81 were picked randomly and have no special meaning. + // 50,000 was chosen so that maybe some spec ids get used more than once. + j := (i + 48) * (i + 67) * (i + 81) + return types.ScopeSpecMetadataAddress(newUUID(j % 50_000)) + } + newScope := func(i int, valueOwnerBase string, owners ...string) types.Scope { + rv := types.Scope{ + ScopeId: newScopeID(i), + SpecificationId: newSpecID(i), + } + if len(valueOwnerBase) > 0 { + rv.ValueOwnerAddress = newAddr(valueOwnerBase).String() + } + for _, owner := range owners { + rv.Owners = append(rv.Owners, types.Party{Address: newAddr(owner).String(), Role: 5}) + } + return rv + } + + app := simapp.Setup(t) + + tests := []struct { + name string + setup func(t *testing.T, ctx sdk.Context) + injSetErrs []string + injUnmarshalErrs []string + expErr string + expLogs []string + expSetCount int + expDelCount int + }{ + { + name: "no scopes", + expLogs: []string{ + "INF Moving scope value owner data into x/bank ledger.", + "INF Done moving scope value owners into bank module. scopes=0 value owners=0", + }, + }, + { + name: "one scope: unmarshal error", + setup: func(t *testing.T, ctx sdk.Context) { + writeScope(t, app.MetadataKeeper, ctx, newScope(123_456_789, "vo_addr1")) + }, + injUnmarshalErrs: []string{"yoko was not wrong"}, + expErr: "error reading scope " + newScopeID(123_456_789).String() + " from state: yoko was not wrong", + expLogs: []string{ + "INF Moving scope value owner data into x/bank ledger.", + "ERR [1]: ScopeID=\"" + newScopeID(123_456_789).String() + + "\" bytes=\"\\n\\x11\\x00_______123456789\\x12\\x11\\x04___________30944*-" + newAddr("vo_addr1").String() + "\"", + }, + }, + { + name: "one scope: set error", + setup: func(t *testing.T, ctx sdk.Context) { + writeScope(t, app.MetadataKeeper, ctx, newScope(23, "vo_addr2")) + }, + injSetErrs: []string{"maybe jethro tull was the greatest"}, + expErr: "could not migrate scope " + newScopeID(23).String() + " value owner \"" + + newAddr("vo_addr2").String() + "\" to bank module: maybe jethro tull was the greatest", + expLogs: []string{ + "INF Moving scope value owner data into x/bank ledger.", + }, + expSetCount: 1, + expDelCount: 0, + }, + { + name: "one scope: no value owner", + setup: func(t *testing.T, ctx sdk.Context) { + writeScope(t, app.MetadataKeeper, ctx, newScope(37373, "")) + }, + expLogs: []string{ + "INF Moving scope value owner data into x/bank ledger.", + "INF Done moving scope value owners into bank module. scopes=1 value owners=0", + }, + expSetCount: 0, + expDelCount: 0, + }, + { + name: "one scope: with value owner", + setup: func(t *testing.T, ctx sdk.Context) { + writeScope(t, app.MetadataKeeper, ctx, newScope(37373, "mineminemine")) + }, + expLogs: []string{ + "INF Moving scope value owner data into x/bank ledger.", + "INF Done moving scope value owners into bank module. scopes=1 value owners=1", + }, + expSetCount: 1, + expDelCount: 2, + }, + { + name: "three scopes: unmarshal error from second", + setup: func(t *testing.T, ctx sdk.Context) { + writeScope(t, app.MetadataKeeper, ctx, newScope(5, "addr1", "addr1"), 1) + writeScope(t, app.MetadataKeeper, ctx, newScope(6, "addr2"), 2) + writeScope(t, app.MetadataKeeper, ctx, newScope(7, "addr3"), 3) + }, + injUnmarshalErrs: []string{"", "radiohead is only okay"}, + expErr: "error reading scope " + newScopeID(6).String() + " from state: radiohead is only okay", + expLogs: []string{ + "INF Moving scope value owner data into x/bank ledger.", + "ERR [2]: ScopeID=\"" + newScopeID(6).String() + + "\" bytes=\"\\n\\x11\\x00_______________6\\x12\\x11\\x04___________42954*-" + newAddr("addr2").String() + "\"", + }, + expSetCount: 1, + expDelCount: 1, + }, + { + name: "three scopes: set error from second", + setup: func(t *testing.T, ctx sdk.Context) { + writeScope(t, app.MetadataKeeper, ctx, newScope(71, "ayyy", "ayyy"), 1) + writeScope(t, app.MetadataKeeper, ctx, newScope(82, "bee"), 2) + writeScope(t, app.MetadataKeeper, ctx, newScope(93, "see"), 3) + }, + injSetErrs: []string{"", "fatboy slim lost that fight"}, + expErr: "could not migrate scope " + newScopeID(82).String() + " value owner \"" + + newAddr("bee").String() + "\" to bank module: fatboy slim lost that fight", + expLogs: []string{ + "INF Moving scope value owner data into x/bank ledger.", + }, + expSetCount: 2, + expDelCount: 1, + }, + { + name: "three scopes: no value owner in second", + setup: func(t *testing.T, ctx sdk.Context) { + writeScope(t, app.MetadataKeeper, ctx, newScope(765, "one", "one"), 1) + writeScope(t, app.MetadataKeeper, ctx, newScope(876, "", "two"), 2) + writeScope(t, app.MetadataKeeper, ctx, newScope(987, "three"), 3) + }, + expLogs: []string{ + "INF Moving scope value owner data into x/bank ledger.", + "INF Done moving scope value owners into bank module. scopes=3 value owners=2", + }, + expSetCount: 2, + expDelCount: 3, // = one from 765 + two from 987 (none from 876), + }, + { + name: "30,005 scopes", + setup: func(t *testing.T, ctx sdk.Context) { + addrs := []string{ + newAddr("one").String(), // cosmos1dahx2h6lta047h6lta047h6lta047h6lq2tdll + newAddr("two").String(), // cosmos1w3mk7h6lta047h6lta047h6lta047h6lakjg9t + newAddr("three").String(), // cosmos1w358yet9ta047h6lta047h6lta047h6lma20rt + newAddr("four").String(), // cosmos1vehh2ujlta047h6lta047h6lta047h6l6dna47 + newAddr("five").String(), // cosmos1ve5hve2lta047h6lta047h6lta047h6ltfdqga + newAddr("six").String(), // cosmos1wd5hsh6lta047h6lta047h6lta047h6la8xq2y + newAddr("seven").String(), // cosmos1wdjhvetwta047h6lta047h6lta047h6ldw3pw9 + newAddr("eight").String(), // cosmos1v45kw6r5ta047h6lta047h6lta047h6l3j2t8s + newAddr("nine").String(), // cosmos1de5kue2lta047h6lta047h6lta047h6lfr0cjd + newAddr("ten").String(), // cosmos1w3jkuh6lta047h6lta047h6lta047h6lqcnhxg + } + + for i := 1; i <= 30_005; i++ { + scope := types.Scope{ + ScopeId: newScopeID(i), + SpecificationId: newSpecID(i), + ValueOwnerAddress: addrs[i%len(addrs)], + } + if i%7 == 0 { + scope.ValueOwnerAddress = "" + } + ownerCount := (i % 3) + 1 // 1 to 3. + if ownerCount > 0 { + scope.Owners = make([]types.Party, ownerCount) + for o := range scope.Owners { + scope.Owners[o].Address = addrs[(i*i+o)%len(addrs)] + scope.Owners[o].Role = types.PartyType(1 + (i+o)%11) // 11 different roles, 1 to 11. + } + } + writeScope(t, app.MetadataKeeper, ctx, scope, i) + } + }, + expLogs: []string{ + "INF Moving scope value owner data into x/bank ledger.", + "INF Progress update: scopes=10000 value owners=8571", + "INF Progress update: scopes=20000 value owners=17143", + "INF Progress update: scopes=30000 value owners=25715", + "INF Done moving scope value owners into bank module. scopes=30005 value owners=25719", + }, + expSetCount: 25719, + expDelCount: 41150, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx, _ := FreshCtx(app).CacheContext() + if tc.setup != nil { + tc.setup(t, ctx) + } + + kpr := newTestKeeper3To4(app.MetadataKeeper) + kpr.setScopeValueOwnerErrs = tc.injSetErrs + kpr.unmarshalErrs = tc.injUnmarshalErrs + + var err error + testFunc := func() { + err = keeper.MigrateValueOwners(ctx, kpr) + } + require.NotPanics(t, testFunc, "migrateValueOwners") + actLogs := kpr.GetLogOutput(t, "migrateValueOwners") + assertions.AssertErrorValue(t, err, tc.expErr, "error from migrateValueOwners") + assert.Equal(t, tc.expLogs, actLogs, "logs messages emitted during migrateValueOwners") + + actSetCount := len(kpr.setScopeValueOwnersCalls) + assert.Equal(t, tc.expSetCount, actSetCount, "calls made to SetScopeValueOwner") + actDelCount := len(kpr.storeCalls.Deletions) + assert.Equal(t, tc.expDelCount, actDelCount, "store deletions made") + }) + } +} + +func TestMigrateValueOwnerToBank(t *testing.T) { + newScopeID := func(b byte) types.MetadataAddress { + rv := make(types.MetadataAddress, 17) + rv[0] = types.ScopeKeyPrefix[0] + for i := 1; i < len(rv); i++ { + rv[i] = b + } + return rv + } + + scopeID := newScopeID('a') + addr := sdk.AccAddress("the_address_________").String() + testlog.WriteVariables(t, "stuff", "scopeID", scopeID, "addr", addr) + tests := []struct { + name string + scope types.Scope + injectErr string + expErr string + }{ + { + name: "error setting value owner", + scope: types.Scope{ScopeId: scopeID, ValueOwnerAddress: addr}, + injectErr: "nickleback was okay", + expErr: "could not migrate scope " + scopeID.String() + " value owner \"" + addr + "\" to bank module: nickleback was okay", + }, + { + name: "all good", + scope: types.Scope{ScopeId: scopeID, ValueOwnerAddress: addr}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + kpr := &testKeeper3To4{} + if len(tc.injectErr) > 0 { + kpr.setScopeValueOwnerErrs = append(kpr.setScopeValueOwnerErrs, tc.injectErr) + } + store := &wrappedKVStore{calls: &storeCalls{}} + expDels := len(tc.expErr) == 0 + expSetCalls := []*provutils.Pair[types.MetadataAddress, string]{ + provutils.NewPair(tc.scope.ScopeId, tc.scope.ValueOwnerAddress), + } + + var err error + testFunc := func() { + err = keeper.MigrateValueOwnerToBank(sdk.Context{}, kpr, store, tc.scope) + } + require.NotPanics(t, testFunc, "migrateValueOwnerToBank") + assertions.AssertErrorValue(t, err, tc.expErr, "error from migrateValueOwnerToBank") + actSetCalls := kpr.setScopeValueOwnersCalls + assert.Equal(t, expSetCalls, actSetCalls, "calls made to SetScopeValueOwner in migrateValueOwnerToBank") + actDels := store.calls.Deletions + if expDels { + assert.NotEmpty(t, actDels, "store deletions") + } else { + assert.Empty(t, actDels, "store deletions") + } + }) + } +} + +func TestDeleteValueOwnerIndexEntries(t *testing.T) { + owner1 := sdk.AccAddress("1_owner_address_____").String() // cosmos1x90k7amwv4e97ctyv3ex2umnta047h6lvq72fg + owner2 := sdk.AccAddress("2_owner_address_____").String() // cosmos1xf0k7amwv4e97ctyv3ex2umnta047h6lw6hvgd + owner3 := sdk.AccAddress("3_owner_address_____").String() // cosmos1xd0k7amwv4e97ctyv3ex2umnta047h6lhtswsw + otherAddr := sdk.AccAddress("other_address_______").String() // cosmos1da6xsetjtaskgerjv4ehxh6lta047h6l3cc9z2 + testlog.WriteVariables(t, "addresses", + "owner1", owner1, + "owner2", owner2, + "owner3", owner3, + "otherAddr", otherAddr, + ) + + newScopeID := func(b byte) types.MetadataAddress { + rv := make(types.MetadataAddress, 17) + rv[0] = types.ScopeKeyPrefix[0] + for i := 1; i < len(rv); i++ { + rv[i] = b + } + return rv + } + scopeID1 := newScopeID('1') // scope1qqcnzvf3xycnzvf3xycnzvf3xycs2xyeyk + scopeID2 := newScopeID('2') // scope1qqeryv3jxgeryv3jxgeryv3jxgeqy48g0a + scopeID3 := newScopeID('3') // scope1qqenxvenxvenxvenxvenxvenxvesqa360g + testlog.WriteVariables(t, "scopes", + "scopeID1", scopeID1, + "scopeID2", scopeID2, + "scopeID3", scopeID3, + ) + + owners := []types.Party{{Address: owner1}, {Address: owner2}, {Address: owner3}} + newScope := func(id types.MetadataAddress, valueOwner string) types.Scope { + return types.Scope{ + ScopeId: id, + Owners: owners, + ValueOwnerAddress: valueOwner, + } + } + + tests := []struct { + name string + scope types.Scope + expDel1 bool + expDel2 bool // if true, expDel1 is also treated as true. + }{ + { + name: "empty value owner address", + scope: newScope(scopeID1, ""), + }, + { + name: "invalid value owner address", + scope: newScope(scopeID1, "nope"), + }, + { + name: "value owner also first owner of three", + scope: newScope(scopeID3, owner1), + expDel1: true, + }, + { + name: "value owner also second owner of three", + scope: newScope(scopeID2, owner2), + expDel1: true, + }, + { + name: "value owner also third owner of three", + scope: newScope(scopeID1, owner3), + expDel1: true, + }, + { + name: "value owner not owner", + scope: newScope(scopeID2, otherAddr), + expDel2: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var expDels [][]byte + if tc.expDel1 || tc.expDel2 { + expDels = make([][]byte, 1, 2) + // If the value owner isn't valid, we shouldn't be expecting any deletions. + vo, _ := sdk.AccAddressFromBech32(tc.scope.ValueOwnerAddress) + expDels[0] = append(expDels[0], 0x18) + expDels[0] = append(expDels[0], byte(len(vo))) + expDels[0] = append(expDels[0], vo...) + expDels[0] = append(expDels[0], tc.scope.ScopeId...) + if tc.expDel2 { + expDels = append(expDels, types.GetAddressScopeCacheKey(vo, tc.scope.ScopeId)) + } + } + + store := &wrappedKVStore{calls: &storeCalls{}} + testFunc := func() { + keeper.DeleteValueOwnerIndexEntries(store, tc.scope) + } + require.NotPanics(t, testFunc, "deleteValueOwnerIndexEntries") + actDels := store.calls.Deletions + assert.Equal(t, expDels, actDels, "store deletions") + }) + } +} diff --git a/x/metadata/keeper/mocks_test.go b/x/metadata/keeper/mocks_test.go index ded2b1af43..cfd46dc99b 100644 --- a/x/metadata/keeper/mocks_test.go +++ b/x/metadata/keeper/mocks_test.go @@ -2,14 +2,21 @@ package keeper_test import ( "context" + "errors" "fmt" + "testing" "time" + "github.com/stretchr/testify/assert" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/x/authz" + markertypes "github.com/provenance-io/provenance/x/marker/types" "github.com/provenance-io/provenance/x/metadata/keeper" + "github.com/provenance-io/provenance/x/metadata/types" ) // This file houses mock stuff for use in unit tests. @@ -325,14 +332,323 @@ func (a *MockAuthorization) Reset() { // String returns a string of this MockAuthorization and satisfies the authz.Authorization interface. func (a *MockAuthorization) String() string { - _ = MockAuthorization{ - Name: "", - AcceptResponse: authz.AcceptResponse{}, - AcceptResponseErr: nil, - AcceptCalls: nil, - } return fmt.Sprintf("MockAuthorization{%q, %v, %v, %d}", a.Name, a.AcceptResponse, a.AcceptResponseErr, a.AcceptCalls) } // ProtoMessage satisfies the authz.Authorization interface. func (a *MockAuthorization) ProtoMessage() {} + +var _ keeper.MarkerKeeper = (*MockMarkerKeeper)(nil) + +// MockMarkerKeeper is a mocked keeper.MarkerKeeper. +type MockMarkerKeeper struct { + IsMarkerAccountResults map[string]bool + IsMarkerAccountCalls []sdk.AccAddress +} + +func NewMockMarkerKeeper() *MockMarkerKeeper { + return &MockMarkerKeeper{ + IsMarkerAccountResults: make(map[string]bool), + } +} + +// WithIsMarkerAccountResults sets up the provided addrs to return true from IsMarkerAccount. +func (k *MockMarkerKeeper) WithIsMarkerAccountResults(addrs ...sdk.AccAddress) *MockMarkerKeeper { + for _, addr := range addrs { + k.IsMarkerAccountResults[string(addr)] = true + } + return k +} + +// ClearResults clears previously recorded calls but leaves the desired results intact. +func (k *MockMarkerKeeper) ClearResults() { + k.IsMarkerAccountCalls = nil +} + +// AssertIsMarkerAccountCalls asserts that calls made to IsMarkerAccount are as expected. +func (k *MockMarkerKeeper) AssertIsMarkerAccountCalls(t *testing.T, exp []sdk.AccAddress) bool { + t.Helper() + act := k.IsMarkerAccountCalls + if assert.Equal(t, exp, act, "Addrs provided to IsMarkerAccount") { + return true + } + expStrs := addrsCastToStrings(exp) + actStrs := addrsCastToStrings(act) + assert.Equal(t, expStrs, actStrs, "Addrs (as strings) provided to IsMarkerAccount") + return false +} + +func (k *MockMarkerKeeper) GetMarkerByDenom(_ sdk.Context, _ string) (markertypes.MarkerAccountI, error) { + panic("MockMarkerKeeper.GetMarkerByDenom not implemented") +} + +func (k *MockMarkerKeeper) IsMarkerAccount(_ sdk.Context, addr sdk.AccAddress) bool { + k.IsMarkerAccountCalls = append(k.IsMarkerAccountCalls, addr) + return k.IsMarkerAccountResults[string(addr)] +} + +// ensure that the MockBankKeeper implements keeper.BankKeeper. +var _ keeper.BankKeeper = (*MockBankKeeper)(nil) + +// MockBankKeeper is a mocked keeper.BankKeeper. +type MockBankKeeper struct { + BlockedAddrResults map[string]bool + MintCoinsResults []string + BurnCoinsResults []string + SendCoinsResults map[string]string + DenomOwnerResults map[string]DenomOwnerResult + + Calls BankKeeperCalls +} + +// BankKeeperCalls contains records of calls made to the mock bank keeper. +type BankKeeperCalls struct { + BlockedAddr []sdk.AccAddress + MintCoins []*MintBurnCall + BurnCoins []*MintBurnCall + SendCoins []*SendCoinsCall + DenomOwner []string +} + +// NewMockBankKeeper creates a new MockBankKeeper. +// Usually followed by calls to WithBlockedAddr, WithMintCoinsErrors, WithBurnCoinsErrors, +// SendCoinsErrors, WithDenomOwnerResult, and/or WithDenomOwnerError. +func NewMockBankKeeper() *MockBankKeeper { + return &MockBankKeeper{ + BlockedAddrResults: make(map[string]bool), + SendCoinsResults: make(map[string]string), + DenomOwnerResults: make(map[string]DenomOwnerResult), + } +} + +// WithBlockedAddr makes the provided addr report as blocked. +func (k *MockBankKeeper) WithBlockedAddr(addr sdk.AccAddress) *MockBankKeeper { + k.BlockedAddrResults[string(addr)] = true + return k +} + +// WithMintCoinsErrors queues up the provided strings as errors to return from MintCoins. +// An entry of "" means no error will be returned for that entry. +func (k *MockBankKeeper) WithMintCoinsErrors(errs ...string) *MockBankKeeper { + k.MintCoinsResults = append(k.MintCoinsResults, errs...) + return k +} + +// WithBurnCoinsErrors queues up the provided strings as errors to return from BurnCoins. +// An entry of "" means no error will be returned for that entry. +func (k *MockBankKeeper) WithBurnCoinsErrors(errs ...string) *MockBankKeeper { + k.BurnCoinsResults = append(k.BurnCoinsResults, errs...) + return k +} + +// WithSendCoinsError makes the SendCoins return the provided err for the given fromAddr. +// An err of "" means no error will be returned for that fromAddr. +func (k *MockBankKeeper) WithSendCoinsError(fromAddr sdk.AccAddress, err string) *MockBankKeeper { + k.SendCoinsResults[string(fromAddr)] = err + return k +} + +// WithDenomOwnerResult makes DenomOwner return the given accAddr for the given scope (with nil error). +func (k *MockBankKeeper) WithDenomOwnerResult(mdAddr types.MetadataAddress, accAddr sdk.AccAddress) *MockBankKeeper { + k.DenomOwnerResults[mdAddr.Denom()] = DenomOwnerResult{Owner: accAddr} + return k +} + +// WithDenomOwnerError makes DenomOwner return the given err for the given scope (with nil AccAddress). +func (k *MockBankKeeper) WithDenomOwnerError(mdAddr types.MetadataAddress, err string) *MockBankKeeper { + k.DenomOwnerResults[mdAddr.Denom()] = DenomOwnerResult{Err: err} + return k +} + +// AssertCalls asserts that all calls made using this bank keeper are equal to the provided expected calls. +func (k *MockBankKeeper) AssertCalls(t *testing.T, exp BankKeeperCalls) bool { + t.Helper() + rv := k.AssertBlockedAddrCalls(t, exp.BlockedAddr) + rv = k.AssertMintCoinsCalls(t, exp.MintCoins) && rv + rv = k.AssertBurnCoinsCalls(t, exp.BurnCoins) && rv + rv = k.AssertSendCoinsCalls(t, exp.SendCoins) && rv + rv = k.AssertDenomOwnerCalls(t, exp.DenomOwner) && rv + return rv +} + +// AssertBlockedAddrCalls asserts that calls made to BlockedAddr are as expected. +func (k *MockBankKeeper) AssertBlockedAddrCalls(t *testing.T, exp []sdk.AccAddress) bool { + t.Helper() + act := k.Calls.BlockedAddr + if assert.Equal(t, exp, act, "Addrs provided to BlockedAddr") { + return true + } + expStrs := addrsCastToStrings(exp) + actStrs := addrsCastToStrings(act) + assert.Equal(t, expStrs, actStrs, "Addrs (as strings) provided to BlockedAddr") + return false +} + +// AssertMintCoinsCalls asserts that calls made to MintCoins are as expected. +func (k *MockBankKeeper) AssertMintCoinsCalls(t *testing.T, exp []*MintBurnCall) bool { + t.Helper() + act := k.Calls.MintCoins + if assert.Equal(t, exp, act, "Calls made to MintCoins") { + return true + } + expStrs := mapToStrings(exp) + actStrs := mapToStrings(act) + assert.Equal(t, expStrs, actStrs, "Calls (as strings) made to MintCoins") + return false +} + +// AssertBurnCoinsCalls asserts that calls made to BurnCoins are as expected. +func (k *MockBankKeeper) AssertBurnCoinsCalls(t *testing.T, exp []*MintBurnCall) bool { + t.Helper() + act := k.Calls.BurnCoins + if assert.Equal(t, exp, act, "Calls made to BurnCoins") { + return true + } + expStrs := mapToStrings(exp) + actStrs := mapToStrings(act) + assert.Equal(t, expStrs, actStrs, "Calls (as strings) made to BurnCoins") + return false +} + +// AssertSendCoinsCalls asserts that calls made to SendCoins are as expected. +func (k *MockBankKeeper) AssertSendCoinsCalls(t *testing.T, exp []*SendCoinsCall) bool { + t.Helper() + act := k.Calls.SendCoins + if assert.Equal(t, exp, act, "Calls made to SendCoins") { + return true + } + expStrs := mapToStrings(exp) + actStrs := mapToStrings(act) + assert.Equal(t, expStrs, actStrs, "Calls (as strings) made to SendCoins") + return false +} + +// AssertDenomOwnerCalls asserts that calls made to DenomOwner are as expected. +func (k *MockBankKeeper) AssertDenomOwnerCalls(t *testing.T, exp []string) bool { + t.Helper() + return assert.Equal(t, exp, k.Calls.DenomOwner, "Calls made to DenomOwner") +} + +// MintBurnCall is the args provided to either MintCoins or BurnCoins. +type MintBurnCall struct { + ModuleName string + Coins sdk.Coins +} + +func (c MintBurnCall) String() string { + return c.ModuleName + "_" + c.Coins.String() +} + +func NewMintBurnCall(moduleName string, coins sdk.Coins) *MintBurnCall { + return &MintBurnCall{ + ModuleName: moduleName, + Coins: coins, + } +} + +// SendCoinsCall is the args provided to SendCoins. +type SendCoinsCall struct { + FromAddr sdk.AccAddress + ToAddr sdk.AccAddress + Amt sdk.Coins +} + +func (c SendCoinsCall) String() string { + return string(c.FromAddr) + "->" + string(c.ToAddr) + ":" + c.Amt.String() +} + +func NewSendCoinsCall(fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) *SendCoinsCall { + return &SendCoinsCall{ + FromAddr: fromAddr, + ToAddr: toAddr, + Amt: amt, + } +} + +// DenomOwnerResult is the args returned by DenomOwner. +type DenomOwnerResult struct { + Owner sdk.AccAddress + Err string +} + +func (k *MockBankKeeper) BlockedAddr(addr sdk.AccAddress) bool { + k.Calls.BlockedAddr = append(k.Calls.BlockedAddr, addr) + return k.BlockedAddrResults[string(addr)] +} + +func (k *MockBankKeeper) MintCoins(_ context.Context, moduleName string, amt sdk.Coins) error { + k.Calls.MintCoins = append(k.Calls.MintCoins, NewMintBurnCall(moduleName, amt)) + if len(k.MintCoinsResults) > 0 { + err := k.MintCoinsResults[0] + k.MintCoinsResults = k.MintCoinsResults[1:] + if len(err) > 0 { + return errors.New(err) + } + } + return nil +} + +func (k *MockBankKeeper) BurnCoins(_ context.Context, moduleName string, amt sdk.Coins) error { + k.Calls.BurnCoins = append(k.Calls.BurnCoins, NewMintBurnCall(moduleName, amt)) + if len(k.BurnCoinsResults) > 0 { + err := k.BurnCoinsResults[0] + k.BurnCoinsResults = k.BurnCoinsResults[1:] + if len(err) > 0 { + return errors.New(err) + } + } + return nil +} + +func (k *MockBankKeeper) SendCoins(_ context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { + k.Calls.SendCoins = append(k.Calls.SendCoins, NewSendCoinsCall(fromAddr, toAddr, amt)) + err := k.SendCoinsResults[string(fromAddr)] + if len(err) > 0 { + return errors.New(err) + } + return nil +} + +func (k *MockBankKeeper) DenomOwner(_ context.Context, denom string) (sdk.AccAddress, error) { + k.Calls.DenomOwner = append(k.Calls.DenomOwner, denom) + result, found := k.DenomOwnerResults[denom] + if found { + if len(result.Err) > 0 { + return nil, errors.New(result.Err) + } + return result.Owner, nil + } + return nil, nil +} + +func (k *MockBankKeeper) GetScopesForValueOwner(_ context.Context, _ sdk.AccAddress, _ *query.PageRequest) (types.AccMDLinks, *query.PageResponse, error) { + panic("not implemented") +} + +// addrsCastToStrings casts each of the provided addrs to strings. +// This does NOT create bech32 address strings. +// It's handy when the bytes of the address are known, but not the bech32, +// but you want to do a string comparison on the slices. +// +// To convert them to bech32 address strings, use mapToStrings. +func addrsCastToStrings(addrs []sdk.AccAddress) []string { + if addrs == nil { + return nil + } + rv := make([]string, len(addrs)) + for i, v := range addrs { + rv[i] = string(v) + } + return rv +} + +func mapToStrings[S ~[]E, E fmt.Stringer](vals S) []string { + if vals == nil { + return nil + } + rv := make([]string, len(vals)) + for i, v := range vals { + rv[i] = v.String() + } + return rv +} diff --git a/x/metadata/keeper/msg_server.go b/x/metadata/keeper/msg_server.go index 69d1c5c23c..8230753d77 100644 --- a/x/metadata/keeper/msg_server.go +++ b/x/metadata/keeper/msg_server.go @@ -12,6 +12,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + markertypes "github.com/provenance-io/provenance/x/marker/types" "github.com/provenance-io/provenance/x/metadata/types" ) @@ -38,11 +39,8 @@ func (k msgServer) WriteScope( //nolint:errcheck // the error was checked when msg.ValidateBasic was called before getting here. msg.ConvertOptionalFields() - var existing *types.Scope - if e, found := k.GetScope(ctx, msg.Scope.ScopeId); found { - existing = &e - } - if err := k.ValidateWriteScope(ctx, existing, msg); err != nil { + transferAgents, err := k.ValidateWriteScope(ctx, msg) + if err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } @@ -52,13 +50,16 @@ func (k msgServer) WriteScope( if msg.UsdMills > 0 { usdMills := sdkmath.NewIntFromUint64(msg.UsdMills) nav := types.NewNetAssetValue(sdk.NewCoin(types.UsdDenom, usdMills), 1) - err := k.AddSetNetAssetValues(ctx, msg.Scope.ScopeId, []types.NetAssetValue{nav}, types.ModuleName) + err = k.AddSetNetAssetValues(ctx, msg.Scope.ScopeId, []types.NetAssetValue{nav}, types.ModuleName) if err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } } - k.SetScope(ctx, msg.Scope) + err = k.SetScope(markertypes.WithTransferAgents(ctx, transferAgents...), msg.Scope) + if err != nil { + return nil, fmt.Errorf("could not write scope %q: %w", msg.Scope.ScopeId, err) + } k.EmitEvent(ctx, types.NewEventTxCompleted(types.TxEndpoint_WriteScope, msg.GetSignerStrs())) return types.NewMsgWriteScopeResponse(msg.Scope.ScopeId), nil @@ -72,11 +73,15 @@ func (k msgServer) DeleteScope( defer telemetry.MeasureSince(time.Now(), types.ModuleName, "tx", "DeleteScope") ctx := UnwrapMetadataContext(goCtx) - if err := k.ValidateDeleteScope(ctx, msg); err != nil { + transferAgents, err := k.ValidateDeleteScope(ctx, msg) + if err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } - k.RemoveScope(ctx, msg.ScopeId) + err = k.RemoveScope(markertypes.WithTransferAgents(ctx, transferAgents...), msg.ScopeId) + if err != nil { + return nil, fmt.Errorf("could not delete scope %q: %w", msg.ScopeId, err) + } k.RemoveNetAssetValues(ctx, msg.ScopeId) @@ -103,7 +108,10 @@ func (k msgServer) AddScopeDataAccess( existing.AddDataAccess(msg.DataAccess) - k.SetScope(ctx, existing) + err := k.SetScope(ctx, existing) + if err != nil { + return nil, fmt.Errorf("could not update scope %q: %w", msg.ScopeId, err) + } k.EmitEvent(ctx, types.NewEventTxCompleted(types.TxEndpoint_AddScopeDataAccess, msg.GetSignerStrs())) return &types.MsgAddScopeDataAccessResponse{}, nil @@ -128,7 +136,10 @@ func (k msgServer) DeleteScopeDataAccess( existing.RemoveDataAccess(msg.DataAccess) - k.SetScope(ctx, existing) + err := k.SetScope(ctx, existing) + if err != nil { + return nil, fmt.Errorf("could not update scope %q: %w", msg.ScopeId, err) + } k.EmitEvent(ctx, types.NewEventTxCompleted(types.TxEndpoint_DeleteScopeDataAccess, msg.GetSignerStrs())) return &types.MsgDeleteScopeDataAccessResponse{}, nil @@ -161,7 +172,10 @@ func (k msgServer) AddScopeOwner( return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } - k.SetScope(ctx, proposed) + err := k.SetScope(ctx, proposed) + if err != nil { + return nil, fmt.Errorf("could not update scope %q: %w", msg.ScopeId, err) + } k.EmitEvent(ctx, types.NewEventTxCompleted(types.TxEndpoint_AddScopeOwner, msg.GetSignerStrs())) return &types.MsgAddScopeOwnerResponse{}, nil @@ -194,7 +208,10 @@ func (k msgServer) DeleteScopeOwner( return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } - k.SetScope(ctx, proposed) + err := k.SetScope(ctx, proposed) + if err != nil { + return nil, fmt.Errorf("could not update scope %q: %w", msg.ScopeId, err) + } k.EmitEvent(ctx, types.NewEventTxCompleted(types.TxEndpoint_DeleteScopeOwner, msg.GetSignerStrs())) return &types.MsgDeleteScopeOwnerResponse{}, nil @@ -208,21 +225,20 @@ func (k msgServer) UpdateValueOwners( defer telemetry.MeasureSince(time.Now(), types.ModuleName, "tx", "UpdateValueOwners") ctx := UnwrapMetadataContext(goCtx) - scopes := make([]*types.Scope, len(msg.ScopeIds)) - for i, id := range msg.ScopeIds { - scope, found := k.GetScope(ctx, id) - if !found { - return nil, sdkerrors.ErrNotFound.Wrapf("scope not found with id %s", id) - } - scopes[i] = &scope + links, err := k.GetScopeValueOwners(ctx, msg.ScopeIds) + if err != nil { + return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } - err := k.ValidateUpdateValueOwners(ctx, scopes, msg.ValueOwnerAddress, msg) + signers, err := k.ValidateUpdateValueOwners(ctx, links, msg.ValueOwnerAddress, msg) if err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } - k.SetScopeValueOwners(ctx, scopes, msg.ValueOwnerAddress) + err = k.SetScopeValueOwners(markertypes.WithTransferAgents(ctx, signers...), links, msg.ValueOwnerAddress) + if err != nil { + return nil, fmt.Errorf("failure setting scope value owners: %w", err) + } k.EmitEvent(ctx, types.NewEventTxCompleted(types.TxEndpoint_UpdateValueOwners, msg.GetSignerStrs())) return &types.MsgUpdateValueOwnersResponse{}, nil @@ -236,27 +252,28 @@ func (k msgServer) MigrateValueOwner( defer telemetry.MeasureSince(time.Now(), types.ModuleName, "tx", "MigrateValueOwner") ctx := UnwrapMetadataContext(goCtx) - var scopes []*types.Scope - err := k.IterateScopesForValueOwner(ctx, msg.Existing, func(scopeID types.MetadataAddress) (stop bool) { - scope, found := k.GetScope(ctx, scopeID) - if found { - scopes = append(scopes, &scope) - } - return false - }) + addr, err := sdk.AccAddressFromBech32(msg.Existing) if err != nil { - return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) + return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid existing address %q: %v", msg.Existing, err) } - if len(scopes) == 0 { - return nil, sdkerrors.ErrNotFound.Wrapf("no scopes found with value owner %q", msg.Existing) + + links, _, err := k.bankKeeper.GetScopesForValueOwner(ctx, addr, nil) + if err != nil { + return nil, sdkerrors.ErrLogic.Wrapf("error getting scopes with value owner %q: %v", addr.String(), err) + } + if len(links) == 0 { + return nil, sdkerrors.ErrNotFound.Wrapf("no scopes found with value owner %q", addr.String()) } - err = k.ValidateUpdateValueOwners(ctx, scopes, msg.Proposed, msg) + signers, err := k.ValidateUpdateValueOwners(ctx, links, msg.Proposed, msg) if err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrap(err.Error()) } - k.SetScopeValueOwners(ctx, scopes, msg.Proposed) + err = k.SetScopeValueOwners(markertypes.WithTransferAgents(ctx, signers...), links, msg.Proposed) + if err != nil { + return nil, fmt.Errorf("failure setting scope value owners: %w", err) + } k.EmitEvent(ctx, types.NewEventTxCompleted(types.TxEndpoint_MigrateValueOwner, msg.GetSignerStrs())) return &types.MsgMigrateValueOwnerResponse{}, nil diff --git a/x/metadata/keeper/msg_server_test.go b/x/metadata/keeper/msg_server_test.go index ca74c2d060..eba3083803 100644 --- a/x/metadata/keeper/msg_server_test.go +++ b/x/metadata/keeper/msg_server_test.go @@ -11,12 +11,19 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256r1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/gogoproto/proto" "github.com/provenance-io/provenance/app" + "github.com/provenance-io/provenance/testutil" + "github.com/provenance-io/provenance/testutil/assertions" + markertypes "github.com/provenance-io/provenance/x/marker/types" "github.com/provenance-io/provenance/x/metadata/keeper" "github.com/provenance-io/provenance/x/metadata/types" ) @@ -28,35 +35,24 @@ type MsgServerTestSuite struct { ctx sdk.Context msgServer types.MsgServer - pubkey1 cryptotypes.PubKey user1 string user1Addr sdk.AccAddress - pubkey2 cryptotypes.PubKey user2 string user2Addr sdk.AccAddress } func (s *MsgServerTestSuite) SetupTest() { s.app = app.Setup(s.T()) - s.ctx = keeper.AddAuthzCacheToContext(s.app.BaseApp.NewContext(false)) + s.ctx = FreshCtx(s.app) s.msgServer = keeper.NewMsgServerImpl(s.app.MetadataKeeper) - s.pubkey1 = secp256k1.GenPrivKey().PubKey() - s.user1Addr = sdk.AccAddress(s.pubkey1.Address()) + s.user1Addr = s.createAccountFromPubKey(secp256k1.GenPrivKey().PubKey()) s.user1 = s.user1Addr.String() - user1Acc := s.app.AccountKeeper.NewAccountWithAddress(s.ctx, s.user1Addr) - s.Require().NoError(user1Acc.SetPubKey(s.pubkey1), "SetPubKey user1") privKey, _ := secp256r1.GenPrivKey() - s.pubkey2 = privKey.PubKey() - s.user2Addr = sdk.AccAddress(s.pubkey2.Address()) + s.user2Addr = s.createAccountFromPubKey(privKey.PubKey()) s.user2 = s.user2Addr.String() - user2Acc := s.app.AccountKeeper.NewAccountWithAddress(s.ctx, s.user2Addr) - s.Require().NoError(user2Acc.SetPubKey(s.pubkey1), "SetPubKey user2") - - s.app.AccountKeeper.SetAccount(s.ctx, user1Acc) - s.app.AccountKeeper.SetAccount(s.ctx, user2Acc) } func TestMsgServerTestSuite(t *testing.T) { @@ -67,11 +63,1015 @@ func TestMsgServerTestSuite(t *testing.T) { // - If errorString is empty, theError must be nil // - If errorString is not empty, theError must equal the errorString. func (s *MsgServerTestSuite) AssertErrorValue(theError error, errorString string, msgAndArgs ...interface{}) bool { - return AssertErrorValue(s.T(), theError, errorString, msgAndArgs...) + return assertions.AssertErrorValue(s.T(), theError, errorString, msgAndArgs...) +} + +// AssertEqualEvents asserts that the expected events equal the actual ones +// in a way that helps identify problems when there are failures. +func (s *MsgServerTestSuite) AssertEqualEvents(expected, actual sdk.Events, msgAndArgs ...interface{}) bool { + return assertions.AssertEqualEvents(s.T(), expected, actual, msgAndArgs...) +} + +// newAddr creates a new sdk.AccAddress using the provided name as the starting bytes. +func newAddr(name string) sdk.AccAddress { + switch { + case len(name) < 20: + // If it's less than 19 bytes long, pad it to 20 chars. + return sdk.AccAddress(name + strings.Repeat("_", 20-len(name))) + case len(name) > 20 && len(name) < 32: + // If it's 21 to 31 bytes long, pad it to 32 chars. + return sdk.AccAddress(name + strings.Repeat("_", 32-len(name))) + } + // If the name is exactly 20 long already, or longer than 32, don't include any padding. + return sdk.AccAddress(name) +} + +// storeUserAccount will create/update the account at the given address. +// The resulting account should not appear to be a smart contract account (e.g. k.IsWasmAccount should return false). +func (s *MsgServerTestSuite) storeUserAccount(addr sdk.AccAddress, pubKey cryptotypes.PubKey) sdk.AccAddress { + acct := s.app.AccountKeeper.GetAccount(s.ctx, addr) + if acct == nil { + acct = s.app.AccountKeeper.NewAccountWithAddress(s.ctx, addr) + } + if acct.GetSequence() == uint64(0) { + s.Require().NoError(acct.SetSequence(1), "%s.SetSequence(1)", addr) + } + if pubKey != nil { + s.Require().NoError(acct.SetPubKey(pubKey), "%s: SetPubKey", addr) + } + s.app.AccountKeeper.SetAccount(s.ctx, acct) + return addr +} + +// createAccountFromPubKey creates/updates an account using the provided public key. +// The newly created account should not appear to be a smart contract account (e.g. k.IsWasmAccount should return false). +func (s *MsgServerTestSuite) createAccountFromPubKey(pubKey cryptotypes.PubKey) sdk.AccAddress { + return s.storeUserAccount(sdk.AccAddress(pubKey.Address()), pubKey) +} + +// setUserAccount creates/updates the account with the given address. +// The resulting account should not appear to be a smart contract account (e.g. k.IsWasmAccount should return false). +func (s *MsgServerTestSuite) setUserAccount(addr sdk.AccAddress) sdk.AccAddress { + return s.storeUserAccount(addr, nil) +} + +// setNamedUserAccount creates/updates an account with an address based off the provided name. +// The resulting account should not appear to be a smart contract account (e.g. k.IsWasmAccount should return false). +func (s *MsgServerTestSuite) setNamedUserAccount(name string) sdk.AccAddress { + return s.storeUserAccount(newAddr(name), nil) +} + +// setNamedSmartContractAccount will create an account that looks like a smart +// contract account and uses the provided name as the basis for its address. +func (s *MsgServerTestSuite) setNamedSmartContractAccount(name string) sdk.AccAddress { + addr := newAddr(name) + acct := s.app.AccountKeeper.NewAccount(s.ctx, &authtypes.BaseAccount{Address: addr.String()}) + s.app.AccountKeeper.SetAccount(s.ctx, acct) + return addr +} + +// newUUID will create a new UUID using the provided name and index to define the bytes. +func (s *MsgServerTestSuite) newUUID(name string, i int) uuid.UUID { + s.T().Helper() + str := fmt.Sprintf("%d_%s", i, name) + if len(str) > 16 { + s.FailNowf("cannot newUUID(%q, %d): base string %q is longer than 16 bytes", name, i, str) + } + if len(str) < 16 { + str = str + strings.Repeat("_", 16-len(str)) + } + rv, err := uuid.FromBytes([]byte(str)) + s.Require().NoError(err, "uuid.FromBytes([]byte(%q))", str) + return rv +} + +// scopeID creates a new Scope MetadataAddress based on the provided number. +func (s *MsgServerTestSuite) scopeID(i int) types.MetadataAddress { + return types.ScopeMetadataAddress(s.newUUID("scope", i)) +} + +// sessionID creates a new Session MetadataAddress based on the provided numbers. +func (s *MsgServerTestSuite) sessionID(i, j int) types.MetadataAddress { + rv, _ := s.scopeID(i).AsSessionAddress(s.newUUID("session", j)) + return rv +} + +// scopeSpecID creates a new ScopeSpecification MetadataAddress based on the provided number. +func (s *MsgServerTestSuite) scopeSpecID(i int) types.MetadataAddress { + return types.ScopeSpecMetadataAddress(s.newUUID("scope_spec", i)) +} + +// untypeEvent calls TypedEventToEvent requiring it to not error. +func (s *MsgServerTestSuite) untypeEvent(event proto.Message) sdk.Event { + rv, err := sdk.TypedEventToEvent(event) + s.Require().NoError(err, "sdk.TypedEventToEvent(%#v)", event) + return rv +} + +// fromBech32 calls AccAddressFromBech32 requiring it to not error. +func (s *MsgServerTestSuite) fromBech32(bech32 string) sdk.AccAddress { + addr, err := sdk.AccAddressFromBech32(bech32) + s.Require().NoError(err, "sdk.AccAddressFromBech32(%q)", bech32) + return addr +} + +// MakeNonWasmAccount will make sure the account with the provided bech32 string has a sequence of 1. +// This will cause the isWasmAccount test to report that the account is NOT a wasm account. +func (s *MsgServerTestSuite) MakeNonWasmAccounts(bech32s ...string) { + s.T().Helper() + for _, bech32 := range bech32s { + addr := s.fromBech32(bech32) + s.setUserAccount(addr) + } +} + +func (s *MsgServerTestSuite) TestWriteScope() { + // It's assumed that individual parts of the WriteScope process are unit tested. + // These tests focus more on module interaction. + + scUserAddr := s.setNamedSmartContractAccount("scUser") // cosmos1wd342um9wf047h6lta047h6lta047h6lj6q23g + userWithWithdrawAddr := s.setNamedUserAccount("userWithWithdraw") // cosmos1w4ek2ujhd96xs4mfw35xgunpwa047h6l3fyz9d + userWithDepositAddr := s.setNamedUserAccount("userWithDeposit") // cosmos1w4ek2ujhd96xs3r9wphhx6t5ta047h6lw5vyqg + userWithBothAddr := s.setNamedUserAccount("userWithBoth") // cosmos1w4ek2ujhd96xssn0w3597h6lta047h6ly0muct + userWithAllAddr := s.setNamedUserAccount("userWithAll") // cosmos1w4ek2ujhd96xsstvd3047h6lta047h6lk0katc + scopeOwnerAddr := s.setNamedUserAccount("scopeOwner") // cosmos1wd342um9wf047h6lta047h6lta047h6lj6q23g + specOwnerAddr := s.setNamedUserAccount("specOwner") // cosmos1wdcx2c60wahx2ujlta047h6lta047h6lw9t9w4 + otherAddr1 := s.setNamedUserAccount("1_other") // cosmos1x90k7argv4e97h6lta047h6lta047h6lfrgsqs + otherAddr2 := s.setNamedUserAccount("2_other") // cosmos1xf0k7argv4e97h6lta047h6lta047h6ltepkp4 + otherAddr3 := s.setNamedUserAccount("3_other") // cosmos1xd0k7argv4e97h6lta047h6lta047h6ljgx5ek + moduleAddr := authtypes.NewModuleAddress(types.ModuleName) // cosmos1g4z8k7hm6hj5fa7s780slnxjvq2dnpgpj2jy0e + + newMarker := func(denom string, withdrawAddr, depAddr sdk.AccAddress) sdk.AccAddress { + addr, err := markertypes.MarkerAddress(denom) + s.Require().NoError(err, "markertypes.MarkerAddress(%q)", denom) + + marker := &markertypes.MarkerAccount{ + BaseAccount: &authtypes.BaseAccount{Address: addr.String()}, + AccessControl: []markertypes.AccessGrant{ + { + Address: userWithBothAddr.String(), + Permissions: markertypes.AccessList{markertypes.Access_Deposit, markertypes.Access_Withdraw}, + }, + { + Address: userWithAllAddr.String(), + Permissions: markertypes.AccessList{ + markertypes.Access_Deposit, markertypes.Access_Withdraw, + markertypes.Access_Mint, markertypes.Access_Burn, markertypes.Access_Delete, + markertypes.Access_Admin, markertypes.Access_Transfer, markertypes.Access_ForceTransfer, + }, + }, + }, + Status: markertypes.StatusProposed, + Denom: denom, + Supply: sdkmath.NewInt(1000), + MarkerType: markertypes.MarkerType_RestrictedCoin, + SupplyFixed: true, + AllowGovernanceControl: true, + } + if len(withdrawAddr) > 0 { + marker.AccessControl = append(marker.AccessControl, markertypes.AccessGrant{ + Address: withdrawAddr.String(), + Permissions: markertypes.AccessList{markertypes.Access_Withdraw}, + }) + } + if len(depAddr) > 0 { + marker.AccessControl = append(marker.AccessControl, markertypes.AccessGrant{ + Address: depAddr.String(), + Permissions: markertypes.AccessList{markertypes.Access_Deposit}, + }) + } + + nav := markertypes.NewNetAssetValue(sdk.NewInt64Coin(denom, 1), 1) + err = s.app.MarkerKeeper.SetNetAssetValue(s.ctx, marker, nav, "testing") + s.Require().NoError(err, "%q: SetNetAssetValue", denom) + err = s.app.MarkerKeeper.AddFinalizeAndActivateMarker(s.ctx, marker) + s.Require().NoError(err, "%q: AddFinalizeAndActivateMarker", denom) + return addr + } + + fromMarkerAddr := newMarker("falcon", userWithWithdrawAddr, nil) // cosmos1wd342um9wfqkgerjta047h6lta047h6lqhvjhz + toMarkerAddr := newMarker("tiger", nil, userWithDepositAddr) // cosmos1w4ek2ujhd96xs4mfw35xgunpwa047h6l3fyz9d + + scopeSpecUUID := s.newUUID("scope_spec", 1) + scopeSpecID := types.ScopeSpecMetadataAddress(scopeSpecUUID) + scopeSpec := types.ScopeSpecification{ + SpecificationId: scopeSpecID, + OwnerAddresses: []string{specOwnerAddr.String()}, + PartiesInvolved: []types.PartyType{types.PartyType_PARTY_TYPE_OWNER}, + } + s.app.MetadataKeeper.SetScopeSpecification(s.ctx, scopeSpec) + + newScope := func(i int, valueOwner sdk.AccAddress, dataAccess ...string) types.Scope { + return types.Scope{ + ScopeId: s.scopeID(i), + SpecificationId: scopeSpecID, + Owners: ownerPartyList(scopeOwnerAddr.String()), + DataAccess: dataAccess, + ValueOwnerAddress: valueOwner.String(), + } + } + setupScope := func(i int, valueOwner sdk.AccAddress, dataAccess ...string) func(ctx sdk.Context) { + return func(ctx sdk.Context) { + scope := newScope(i, valueOwner, dataAccess...) + err := s.app.MetadataKeeper.SetScope(markertypes.WithTransferAgents(ctx, userWithAllAddr), scope) + s.Require().NoError(err, "setupScope: SetScope %d, %q", i, scope.ValueOwnerAddress) + } + } + newSCScope := func(i int, valueOwner sdk.AccAddress, dataAccess ...string) types.Scope { + return types.Scope{ + ScopeId: s.scopeID(i), + SpecificationId: scopeSpecID, + Owners: []types.Party{ + {Address: scopeOwnerAddr.String(), Role: types.PartyType_PARTY_TYPE_OWNER}, + {Address: scUserAddr.String(), Role: types.PartyType_PARTY_TYPE_PROVENANCE}, + }, + DataAccess: dataAccess, + ValueOwnerAddress: valueOwner.String(), + } + } + setupSCScope := func(i int, valueOwner sdk.AccAddress, dataAccess ...string) func(ctx sdk.Context) { + return func(ctx sdk.Context) { + scope := newSCScope(i, valueOwner, dataAccess...) + err := s.app.MetadataKeeper.SetScope(markertypes.WithTransferAgents(ctx, userWithAllAddr), scope) + s.Require().NoError(err, "setupSCScope: SetScope %d, %q", i, scope.ValueOwnerAddress) + } + } + + tests := []struct { + name string + setup func(ctx sdk.Context) + msg types.MsgWriteScopeRequest + expErr string + // expScope is the scope (including value owner) that is expected after a successful WriteScope call. + // If not defined, msg.Scope will be used. + expScope *types.Scope + // expEventsNAV should be true if you expect a NAV event to be emitted. + expEventsNAV bool + // expEventsMint should be true if you expect events related to minting a coin to be emitted. + expEventsMint bool + // expEventsTrans should be the "from" address for the transfer events (if the transfer events are expected). + // If true, the expEventsTrans field is ignored, and the moduleAddr is used for that. + expEventsTrans sdk.AccAddress + // expEventsTransErr should be true if you expect an error from the SendCoins call. When that happens, + // only some of the transfer events get emitted, but you'll need to also provide an expEventsTrans. + expEventsTransErr bool + // expEventsCreate should be true if you expected a EventScopeCreated to be emitted. + expEventsCreate bool + // expEventsCreate should be true if you expected a EventScopeUpdated to be emitted. + expEventsUpdate bool + }{ + { + name: "invalid scope", + msg: types.MsgWriteScopeRequest{ + Scope: types.Scope{ + ScopeId: s.scopeID(1)[:2], + SpecificationId: scopeSpecID, + Owners: ownerPartyList(scopeOwnerAddr.String()), + }, + Signers: []string{scopeOwnerAddr.String()}, + }, + expErr: "invalid scope metadata address MetadataAddress{0x0, 0x31}: " + + "incorrect address length (expected: 17, actual: 2): invalid request", + }, + { + name: "new scope with value owner", + msg: types.MsgWriteScopeRequest{ + Scope: newScope(2, otherAddr2), + Signers: []string{scopeOwnerAddr.String()}, + UsdMills: 555, + }, + expEventsNAV: true, + expEventsMint: true, + expEventsCreate: true, + }, + { + name: "new scope without value owner", + msg: types.MsgWriteScopeRequest{ + Scope: newScope(2, nil), + Signers: []string{otherAddr1.String()}, + }, + expEventsCreate: true, + }, + { + name: "using optional fields", + msg: types.MsgWriteScopeRequest{ + Scope: types.Scope{Owners: ownerPartyList(scopeOwnerAddr.String())}, + Signers: []string{scopeOwnerAddr.String()}, + ScopeUuid: s.newUUID("scope", 4).String(), + SpecUuid: scopeSpecUUID.String(), + }, + expScope: &types.Scope{ + ScopeId: s.scopeID(4), + SpecificationId: scopeSpecID, + Owners: ownerPartyList(scopeOwnerAddr.String()), + }, + expEventsCreate: true, + }, + { + name: "updating scope: already has value owner, but no value owner in request", + setup: setupScope(10, otherAddr2), + msg: types.MsgWriteScopeRequest{ + Scope: types.Scope{ + ScopeId: s.scopeID(10), + SpecificationId: scopeSpecID, + Owners: ownerPartyList(scopeOwnerAddr.String()), + DataAccess: []string{otherAddr3.String()}, + ValueOwnerAddress: "", + }, + Signers: []string{scopeOwnerAddr.String()}, + UsdMills: 12345, + }, + expScope: &types.Scope{ + ScopeId: s.scopeID(10), + SpecificationId: scopeSpecID, + Owners: ownerPartyList(scopeOwnerAddr.String()), + DataAccess: []string{otherAddr3.String()}, + ValueOwnerAddress: otherAddr2.String(), + }, + expEventsNAV: true, + expEventsUpdate: true, + }, + { + name: "value owner change: empty to user", + setup: setupScope(11, nil), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(11, otherAddr2), + Signers: []string{scopeOwnerAddr.String()}, + }, + expEventsMint: true, + expEventsUpdate: true, + }, + { + name: "value owner change: empty to marker: no signer with deposit", + setup: setupScope(12, nil), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(12, toMarkerAddr), + Signers: []string{scopeOwnerAddr.String()}, + }, + expErr: "could not write scope \"" + s.scopeID(12).String() + "\": could not set value owner: " + + "could not send scope coin \"1nft/" + s.scopeID(12).String() + "\" " + + "from " + moduleAddr.String() + " to " + toMarkerAddr.String() + ": " + + scopeOwnerAddr.String() + " does not have ACCESS_DEPOSIT on " + + "tiger marker (" + toMarkerAddr.String() + ")", + expEventsMint: true, + expEventsTransErr: true, + }, + { + name: "value owner change: empty to marker: signer with withdraw on other marker", + setup: setupScope(12, nil), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(12, toMarkerAddr), + Signers: []string{scopeOwnerAddr.String(), userWithWithdrawAddr.String()}, + }, + expErr: "could not write scope \"" + s.scopeID(12).String() + "\": could not set value owner: " + + "could not send scope coin \"1nft/" + s.scopeID(12).String() + "\" " + + "from " + moduleAddr.String() + " to " + toMarkerAddr.String() + ": " + + "none of [\"" + scopeOwnerAddr.String() + "\" \"" + userWithWithdrawAddr.String() + "\"] have permission ACCESS_DEPOSIT on " + + "tiger marker (" + toMarkerAddr.String() + ")", + expEventsMint: true, + expEventsTransErr: true, + }, + { + name: "value owner change: empty to marker: signer with deposit", + setup: setupScope(13, nil), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(13, toMarkerAddr), + Signers: []string{scopeOwnerAddr.String(), userWithDepositAddr.String()}, + }, + expEventsMint: true, + expEventsUpdate: true, + }, + { + name: "value owner change: user to user: wrong signer", + setup: setupScope(14, otherAddr1), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(14, otherAddr2), + Signers: []string{scopeOwnerAddr.String()}, + }, + expErr: "missing signature from existing value owner \"" + otherAddr1.String() + "\": invalid request", + }, + { + name: "value owner change: user to user: right signer", + setup: setupScope(14, otherAddr1), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(14, otherAddr2), + Signers: []string{otherAddr1.String()}, + }, + expEventsTrans: otherAddr1, + expEventsUpdate: true, + }, + { + name: "value owner change: marker to user: no signer with withdraw", + setup: setupScope(15, fromMarkerAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(15, otherAddr2), + Signers: []string{scopeOwnerAddr.String()}, + }, + expErr: "could not write scope \"" + s.scopeID(15).String() + "\": could not set value owner: " + + "could not send scope coin \"1nft/" + s.scopeID(15).String() + "\" " + + "from " + fromMarkerAddr.String() + " to " + otherAddr2.String() + ": " + + scopeOwnerAddr.String() + " does not have ACCESS_WITHDRAW on " + + "falcon marker (" + fromMarkerAddr.String() + ")", + expEventsTrans: fromMarkerAddr, + expEventsTransErr: true, + }, + { + name: "value owner change: marker to user: signer with withdraw", + setup: setupScope(16, fromMarkerAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(16, otherAddr2), + Signers: []string{userWithWithdrawAddr.String()}, + }, + expEventsTrans: fromMarkerAddr, + expEventsUpdate: true, + }, + { + name: "value owner change: user to marker: not signed by user", + setup: setupScope(17, otherAddr1), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(17, toMarkerAddr), + Signers: []string{scopeOwnerAddr.String()}, + }, + expErr: "missing signature from existing value owner \"" + otherAddr1.String() + "\": invalid request", + }, + { + name: "value owner change: user to marker: signer does not have deposit", + setup: setupScope(18, userWithWithdrawAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(18, toMarkerAddr), + Signers: []string{userWithWithdrawAddr.String()}, + }, + expErr: "could not write scope \"" + s.scopeID(18).String() + "\": could not set value owner: " + + "could not send scope coin \"1nft/" + s.scopeID(18).String() + "\" " + + "from " + userWithWithdrawAddr.String() + " to " + toMarkerAddr.String() + ": " + + userWithWithdrawAddr.String() + " does not have ACCESS_DEPOSIT on " + + "tiger marker (" + toMarkerAddr.String() + ")", + expEventsTrans: userWithWithdrawAddr, + expEventsTransErr: true, + }, + { + name: "value owner change: user to marker: signer has deposit", + setup: setupScope(19, userWithDepositAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(19, toMarkerAddr), + Signers: []string{userWithDepositAddr.String()}, + }, + expEventsTrans: userWithDepositAddr, + expEventsUpdate: true, + }, + { + name: "value owner change: user to marker: other signer has deposit", + setup: setupScope(19, otherAddr3), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(19, toMarkerAddr), + Signers: []string{otherAddr3.String(), userWithDepositAddr.String()}, + }, + expEventsTrans: otherAddr3, + expEventsUpdate: true, + }, + { + name: "value owner change: marker to marker: no signers with permissions", + setup: setupScope(30, fromMarkerAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(30, toMarkerAddr), + Signers: []string{otherAddr1.String(), otherAddr2.String(), otherAddr3.String()}, + }, + expErr: "could not write scope \"" + s.scopeID(30).String() + "\": could not set value owner: " + + "could not send scope coin \"1nft/" + s.scopeID(30).String() + "\" " + + "from " + fromMarkerAddr.String() + " to " + toMarkerAddr.String() + ": " + + "none of [\"" + otherAddr1.String() + "\" \"" + otherAddr2.String() + "\" \"" + otherAddr3.String() + + "\"] have permission ACCESS_WITHDRAW on falcon marker (" + fromMarkerAddr.String() + ")", + expEventsTrans: fromMarkerAddr, + expEventsTransErr: true, + }, + { + name: "value owner change: marker to marker: with only withdraw", + setup: setupScope(31, fromMarkerAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(31, toMarkerAddr), + Signers: []string{userWithWithdrawAddr.String()}, + }, + expErr: "could not write scope \"" + s.scopeID(31).String() + "\": could not set value owner: " + + "could not send scope coin \"1nft/" + s.scopeID(31).String() + "\" " + + "from " + fromMarkerAddr.String() + " to " + toMarkerAddr.String() + ": " + + userWithWithdrawAddr.String() + " does not have ACCESS_DEPOSIT on " + + "tiger marker (" + toMarkerAddr.String() + ")", + expEventsTrans: fromMarkerAddr, + expEventsTransErr: true, + }, + { + name: "value owner change: marker to marker: with only deposit", + setup: setupScope(32, fromMarkerAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(32, toMarkerAddr), + Signers: []string{userWithDepositAddr.String()}, + }, + expErr: "could not write scope \"" + s.scopeID(32).String() + "\": could not set value owner: " + + "could not send scope coin \"1nft/" + s.scopeID(32).String() + "\" " + + "from " + fromMarkerAddr.String() + " to " + toMarkerAddr.String() + ": " + + userWithDepositAddr.String() + " does not have ACCESS_WITHDRAW on " + + "falcon marker (" + fromMarkerAddr.String() + ")", + expEventsTrans: fromMarkerAddr, + expEventsTransErr: true, + }, + { + name: "value owner change: marker to marker: one signer with both permissions", + setup: setupScope(35, fromMarkerAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(35, toMarkerAddr), + Signers: []string{userWithBothAddr.String()}, + }, + expEventsTrans: fromMarkerAddr, + expEventsUpdate: true, + }, + { + name: "value owner change: marker to marker: different signers with deposit and withdraw", + setup: setupScope(36, fromMarkerAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(36, toMarkerAddr), + Signers: []string{userWithDepositAddr.String(), userWithWithdrawAddr.String()}, + }, + expEventsTrans: fromMarkerAddr, + expEventsUpdate: true, + }, + { + name: "value owner change: marker to marker: different signers with withdraw and deposit", + setup: setupScope(37, fromMarkerAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newScope(37, toMarkerAddr), + Signers: []string{userWithWithdrawAddr.String(), userWithDepositAddr.String()}, + }, + expEventsTrans: fromMarkerAddr, + expEventsUpdate: true, + }, + { + name: "value owner change: smart contract to marker with ignored transfer agent", + setup: setupSCScope(38, scUserAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newSCScope(38, toMarkerAddr), + Signers: []string{scUserAddr.String(), userWithDepositAddr.String()}, + }, + // Because the first signer is a smart contract, the other signers should not be considered + // when transferring the scope coin. That means that this error should not contain them. + expErr: "could not write scope \"" + s.scopeID(38).String() + "\": could not set value owner: " + + "could not send scope coin \"1nft/" + s.scopeID(38).String() + "\" " + + "from " + scUserAddr.String() + " to " + toMarkerAddr.String() + ": " + + scUserAddr.String() + " does not have ACCESS_DEPOSIT on " + + "tiger marker (" + toMarkerAddr.String() + ")", + expEventsTrans: scUserAddr, + expEventsTransErr: true, + }, + { + name: "value owner and data access change: smart contract to marker with ignored transfer agent", + setup: setupSCScope(39, scUserAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newSCScope(39, toMarkerAddr, otherAddr1.String()), + Signers: []string{scUserAddr.String(), scopeOwnerAddr.String(), userWithBothAddr.String()}, + }, + // The scUserAddr and scopeOwnerAddr signers are used to allow the data access change. + // But since the first signer is a smart contract, the other two signers should not be considered + // when transferring the scope coin. That means that this error should not contain them. + expErr: "could not write scope \"" + s.scopeID(39).String() + "\": could not set value owner: " + + "could not send scope coin \"1nft/" + s.scopeID(39).String() + "\" " + + "from " + scUserAddr.String() + " to " + toMarkerAddr.String() + ": " + + scUserAddr.String() + " does not have ACCESS_DEPOSIT on " + + "tiger marker (" + toMarkerAddr.String() + ")", + expEventsTrans: scUserAddr, + expEventsTransErr: true, + }, + { + name: "value owner change: smart contract to user", + setup: setupSCScope(40, scUserAddr), + msg: types.MsgWriteScopeRequest{ + Scope: newSCScope(40, otherAddr2), + Signers: []string{scUserAddr.String()}, + }, + expEventsTrans: scUserAddr, + expEventsUpdate: true, + }, + { + name: "value owner change: user to smart contract by smart contract", + setup: setupSCScope(41, otherAddr1), + msg: types.MsgWriteScopeRequest{ + Scope: newSCScope(40, scUserAddr), + Signers: []string{scUserAddr.String(), otherAddr1.String()}, + }, + // Because the first signer is a smart contract, the second signer is ignored for the purposes of + // validating a change to the value owner. So even though they're in the signers list, they don't count. + expErr: "smart contract signer " + scUserAddr.String() + " is not authorized: invalid request", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + // Set the expected scope if it wasn't provided. + if tc.expScope == nil { + tc.expScope = &tc.msg.Scope + } + + // Identify the id of the scope in question so we can use it in the expected stuff. + var scopeID types.MetadataAddress + switch { + case tc.expScope != nil && len(tc.expScope.ScopeId) > 0: + scopeID = tc.expScope.ScopeId + case len(tc.msg.Scope.ScopeId) > 0: + scopeID = tc.msg.Scope.ScopeId + case len(tc.msg.ScopeUuid) > 0: + uid, err := uuid.Parse(tc.msg.ScopeUuid) + s.Require().NoError(err, "uuid.Parse(%q)", tc.msg.ScopeUuid) + types.ScopeMetadataAddress(uid) + } + + // Create the expected response. + var expResp *types.MsgWriteScopeResponse + if len(tc.expErr) == 0 { + expResp = types.NewMsgWriteScopeResponse(scopeID) + } + + // Create the list of expected events. + eventsBuilder := testutil.NewEventsBuilder(s.T()) + if tc.expEventsNAV { + eventsBuilder.AddTypedEvent(&types.EventSetNetAssetValue{ + ScopeId: scopeID.String(), + Price: fmt.Sprintf("%dusd", tc.msg.UsdMills), + Source: types.ModuleName, + Volume: "1", + }) + } + if tc.expEventsMint || len(tc.expEventsTrans) > 0 { + var from string + if tc.expEventsMint { + from = moduleAddr.String() + } else { + from = tc.expEventsTrans.String() + } + to := tc.expScope.ValueOwnerAddress + amount := scopeID.Coin().String() + if tc.expEventsMint { + eventsBuilder.AddMintCoinsStrs(from, amount) + } + if tc.expEventsTransErr { + eventsBuilder.AddFailedSendCoinsStrs(from, amount) + } else { + eventsBuilder.AddSendCoinsStrs(from, to, amount) + } + } + if tc.expEventsCreate { + eventsBuilder.AddTypedEvent(types.NewEventScopeCreated(scopeID)) + } + if tc.expEventsUpdate { + eventsBuilder.AddTypedEvent(types.NewEventScopeUpdated(scopeID)) + } + if len(tc.expErr) == 0 { + eventsBuilder.AddTypedEvent(types.NewEventTxCompleted(types.TxEndpoint_WriteScope, tc.msg.Signers)) + } + expEvents := eventsBuilder.Build() + + // Use a cache context so that each case is independent. + ctx, _ := s.ctx.CacheContext() + if tc.setup != nil { + tc.setup(ctx) + } + + em := sdk.NewEventManager() + ctx = ctx.WithEventManager(em) + var actResp *types.MsgWriteScopeResponse + var err error + testFunc := func() { + actResp, err = s.msgServer.WriteScope(ctx, &tc.msg) + } + s.Require().NotPanics(testFunc, "msgServer.WriteScope") + s.AssertErrorValue(err, tc.expErr, "error from msgServer.WriteScope") + s.Assert().Equal(expResp, actResp, "response from msgServer.WriteScope") + + actEvents := em.Events() + s.AssertEqualEvents(expEvents, actEvents, "events emitted during msgServer.WriteScope") + + if err == nil && len(tc.expErr) == 0 { + actScope, found := s.app.MetadataKeeper.GetScopeWithValueOwner(ctx, tc.expScope.ScopeId) + if s.Assert().True(found, "found bool from GetScopeWithValueOwner after msgServer.WriteScope") { + s.Assert().Equal(tc.expScope, &actScope, "scope after msgServer.WriteScope") + } + } + }) + } } -// TODO: WriteScope tests -// TODO: DeleteScope tests +func (s *MsgServerTestSuite) TestDeleteScope() { + scUserAddr := s.setNamedSmartContractAccount("scUser") // cosmos1wd342um9wf047h6lta047h6lta047h6lj6q23g + scopeOwnerAddr := s.setNamedUserAccount("scopeOwner") // cosmos1wd342um9wf047h6lta047h6lta047h6lj6q23g + otherAddr1 := s.setNamedUserAccount("1_other") // cosmos1x90k7argv4e97h6lta047h6lta047h6lfrgsqs + otherAddr2 := s.setNamedUserAccount("2_other") // cosmos1xf0k7argv4e97h6lta047h6lta047h6ltepkp4 + moduleAddr := authtypes.NewModuleAddress(types.ModuleName) // cosmos1g4z8k7hm6hj5fa7s780slnxjvq2dnpgpj2jy0e + + recordID := func(scopeI int, suffix string) types.MetadataAddress { + return s.scopeID(scopeI).MustGetAsRecordAddress("record_" + suffix) + } + newRecord := func(suffix string, scopeI, sessionI int) *types.Record { + rv := &types.Record{ + Name: "record_" + suffix, + SessionId: s.sessionID(scopeI, sessionI), + Process: types.Process{ + Name: "process_name_" + suffix, + ProcessId: &types.Process_Hash{Hash: "process_id_hash_" + suffix}, + Method: "process_method_" + suffix, + }, + Inputs: []types.RecordInput{ + { + Name: "inputs[0]_name_" + suffix, + Source: &types.RecordInput_Hash{Hash: "inputs[0]_source_hash_" + suffix}, + TypeName: "inputs[0]_type_" + suffix, + Status: types.RecordInputStatus_Record, + }, + }, + Outputs: []types.RecordOutput{ + { + Hash: "outputs[0]_hash_" + suffix, + Status: types.ResultStatus_RESULT_STATUS_PASS, + }, + }, + } + rv.SpecificationId = types.RecordSpecMetadataAddress(s.newUUID("cspec", sessionI), rv.Name) + return rv + } + + tests := []struct { + name string + setup func(ctx sdk.Context) + msg types.MsgDeleteScopeRequest + expErr string + expEvents sdk.Events + }{ + { + name: "no such scope", + msg: types.MsgDeleteScopeRequest{ + ScopeId: s.scopeID(1), + Signers: []string{otherAddr1.String()}, + }, + expErr: "scope not found with id " + s.scopeID(1).String() + ": invalid request", + }, + { + name: "just the scope to delete", + setup: func(ctx sdk.Context) { + scope := types.Scope{ + ScopeId: s.scopeID(2), + SpecificationId: s.scopeSpecID(2), + Owners: ownerPartyList(scopeOwnerAddr.String()), + } + err := s.app.MetadataKeeper.SetScope(ctx, scope) + s.Require().NoError(err, "SetScope") + }, + msg: types.MsgDeleteScopeRequest{ + ScopeId: s.scopeID(2), + Signers: []string{scopeOwnerAddr.String()}, + }, + expEvents: testutil.NewEventsBuilder(s.T()). + AddTypedEvent(types.NewEventScopeDeleted(s.scopeID(2))). + Build(), + }, + { + name: "with value owner but no sessions or records", + setup: func(ctx sdk.Context) { + scope := types.Scope{ + ScopeId: s.scopeID(3), + SpecificationId: s.scopeSpecID(3), + Owners: ownerPartyList(scopeOwnerAddr.String()), + ValueOwnerAddress: otherAddr2.String(), + } + err := s.app.MetadataKeeper.SetScope(ctx, scope) + s.Require().NoError(err, "SetScope") + }, + msg: types.MsgDeleteScopeRequest{ + ScopeId: s.scopeID(3), + Signers: []string{scopeOwnerAddr.String(), otherAddr2.String()}, + }, + expEvents: testutil.NewEventsBuilder(s.T()). + AddSendCoins(otherAddr2, moduleAddr, s.scopeID(3).Coins()). + AddBurnCoinsStrs(moduleAddr.String(), s.scopeID(3).Coins().String()). + AddTypedEvent(types.NewEventScopeDeleted(s.scopeID(3))). + Build(), + }, + { + name: "one record one session", + setup: func(ctx sdk.Context) { + writeData(s.T(), ctx, s.app.MetadataKeeper, &dataSetup{ + Scopes: []*types.Scope{{ + ScopeId: s.scopeID(3), + SpecificationId: s.scopeSpecID(3), + Owners: ownerPartyList(scopeOwnerAddr.String()), + }}, + Sessions: [][]*types.Session{{{ + SessionId: s.sessionID(3, 1), + SpecificationId: types.ContractSpecMetadataAddress(s.newUUID("cspec", 1)), + Parties: ownerPartyList(scopeOwnerAddr.String()), + Name: "first", + }}}, + Records: [][][]*types.Record{{{newRecord("one", 3, 1)}}}, + }) + }, + msg: types.MsgDeleteScopeRequest{ + ScopeId: s.scopeID(3), + Signers: []string{scopeOwnerAddr.String()}, + }, + expEvents: testutil.NewEventsBuilder(s.T()). + AddTypedEvent(types.NewEventRecordDeleted(recordID(3, "one"))). + AddTypedEvent(types.NewEventSessionDeleted(s.sessionID(3, 1))). + AddTypedEvent(types.NewEventScopeDeleted(s.scopeID(3))). + Build(), + }, + { + name: "one record one session with value owner", + setup: func(ctx sdk.Context) { + writeData(s.T(), ctx, s.app.MetadataKeeper, &dataSetup{ + Scopes: []*types.Scope{{ + ScopeId: s.scopeID(4), + SpecificationId: s.scopeSpecID(4), + Owners: ownerPartyList(scopeOwnerAddr.String()), + ValueOwnerAddress: otherAddr1.String(), + }}, + Sessions: [][]*types.Session{{{ + SessionId: s.sessionID(4, 1), + SpecificationId: types.ContractSpecMetadataAddress(s.newUUID("cspec", 1)), + Parties: ownerPartyList(scopeOwnerAddr.String()), + Name: "first", + }}}, + Records: [][][]*types.Record{{{newRecord("one", 4, 1)}}}, + }) + }, + msg: types.MsgDeleteScopeRequest{ + ScopeId: s.scopeID(4), + Signers: []string{scopeOwnerAddr.String(), otherAddr1.String()}, + }, + expEvents: testutil.NewEventsBuilder(s.T()). + AddSendCoins(otherAddr1, moduleAddr, s.scopeID(4).Coins()). + AddBurnCoinsStrs(moduleAddr.String(), s.scopeID(4).Coins().String()). + AddTypedEvent(types.NewEventRecordDeleted(recordID(4, "one"))). + AddTypedEvent(types.NewEventSessionDeleted(s.sessionID(4, 1))). + AddTypedEvent(types.NewEventScopeDeleted(s.scopeID(4))). + Build(), + }, + { + name: "value owner and two sessions with one record and three records and navs", + setup: func(ctx sdk.Context) { + writeData(s.T(), ctx, s.app.MetadataKeeper, &dataSetup{ + Scopes: []*types.Scope{{ + ScopeId: s.scopeID(5), + SpecificationId: s.scopeSpecID(5), + Owners: ownerPartyList(scopeOwnerAddr.String()), + ValueOwnerAddress: otherAddr2.String(), + }}, + Sessions: [][]*types.Session{{ + { + SessionId: s.sessionID(5, 1), + SpecificationId: types.ContractSpecMetadataAddress(s.newUUID("cspec", 1)), + Parties: ownerPartyList(scopeOwnerAddr.String()), + Name: "first", + }, + { + SessionId: s.sessionID(5, 2), + SpecificationId: types.ContractSpecMetadataAddress(s.newUUID("cspec", 2)), + Parties: ownerPartyList(scopeOwnerAddr.String()), + Name: "second", + }, + }}, + Records: [][][]*types.Record{{ + {newRecord("one", 5, 1)}, + {newRecord("two", 5, 2)}, + {newRecord("three", 5, 2)}, + {newRecord("four", 5, 2)}, + }}, + }) + navs := []types.NetAssetValue{ + {Price: sdk.NewInt64Coin("alabama", 22)}, + {Price: sdk.NewInt64Coin("california", 31)}, + {Price: sdk.NewInt64Coin("delaware", 1)}, + {Price: sdk.NewInt64Coin("hawaii", 50)}, + } + for i, nav := range navs { + err := s.app.MetadataKeeper.SetNetAssetValue(ctx, s.scopeID(5), nav, "testing") + s.Require().NoError(err, "[%d]: SetNetAssetValue(%#v)", i, nav) + } + }, + msg: types.MsgDeleteScopeRequest{ + ScopeId: s.scopeID(5), + Signers: []string{scopeOwnerAddr.String(), otherAddr2.String()}, + }, + expEvents: testutil.NewEventsBuilder(s.T()). + AddSendCoins(otherAddr2, moduleAddr, s.scopeID(5).Coins()). + AddBurnCoinsStrs(moduleAddr.String(), s.scopeID(5).Coins().String()). + AddTypedEvent(types.NewEventRecordDeleted(recordID(5, "two"))). + AddTypedEvent(types.NewEventRecordDeleted(recordID(5, "four"))). + AddTypedEvent(types.NewEventRecordDeleted(recordID(5, "one"))). + AddTypedEvent(types.NewEventSessionDeleted(s.sessionID(5, 1))). + AddTypedEvent(types.NewEventRecordDeleted(recordID(5, "three"))). + AddTypedEvent(types.NewEventSessionDeleted(s.sessionID(5, 2))). + AddTypedEvent(types.NewEventScopeDeleted(s.scopeID(5))). + Build(), + }, + { + name: "smart contract signer for scope with other value owner", + setup: func(ctx sdk.Context) { + scope := types.Scope{ + ScopeId: s.scopeID(6), + SpecificationId: s.scopeSpecID(6), + Owners: ownerPartyList(scUserAddr.String()), + ValueOwnerAddress: otherAddr2.String(), + } + err := s.app.MetadataKeeper.SetScope(ctx, scope) + s.Require().NoError(err, "SetScope") + }, + msg: types.MsgDeleteScopeRequest{ + ScopeId: s.scopeID(6), + Signers: []string{scUserAddr.String(), otherAddr2.String()}, + }, + // Since the first signer is a smart contract, the second cannot sign as value owner. + expErr: "missing signature from existing value owner \"" + otherAddr2.String() + "\": invalid request", + }, + { + name: "not signed by value owner", + setup: func(ctx sdk.Context) { + scope := types.Scope{ + ScopeId: s.scopeID(7), + SpecificationId: s.scopeSpecID(7), + Owners: ownerPartyList(scopeOwnerAddr.String()), + ValueOwnerAddress: otherAddr2.String(), + } + err := s.app.MetadataKeeper.SetScope(ctx, scope) + s.Require().NoError(err, "SetScope") + }, + msg: types.MsgDeleteScopeRequest{ + ScopeId: s.scopeID(7), + Signers: []string{scopeOwnerAddr.String()}, + }, + expErr: "missing signature from existing value owner \"" + otherAddr2.String() + "\": invalid request", + }, + { + name: "not signed by owner", + setup: func(ctx sdk.Context) { + scope := types.Scope{ + ScopeId: s.scopeID(8), + SpecificationId: s.scopeSpecID(8), + Owners: ownerPartyList(scopeOwnerAddr.String()), + ValueOwnerAddress: otherAddr2.String(), + } + err := s.app.MetadataKeeper.SetScope(ctx, scope) + s.Require().NoError(err, "SetScope") + }, + msg: types.MsgDeleteScopeRequest{ + ScopeId: s.scopeID(8), + Signers: []string{otherAddr2.String()}, + }, + expErr: "missing signature: " + scopeOwnerAddr.String() + ": invalid request", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var expResp *types.MsgDeleteScopeResponse + if len(tc.expErr) == 0 { + expResp = &types.MsgDeleteScopeResponse{} + event := s.untypeEvent(types.NewEventTxCompleted(types.TxEndpoint_DeleteScope, tc.msg.Signers)) + tc.expEvents = append(tc.expEvents, event) + } + + // Use a cache context so that each case is independent. + ctx, _ := s.ctx.CacheContext() + if tc.setup != nil { + tc.setup(ctx) + } + + em := sdk.NewEventManager() + ctx = ctx.WithEventManager(em) + var actResp *types.MsgDeleteScopeResponse + var err error + testFunc := func() { + actResp, err = s.msgServer.DeleteScope(ctx, &tc.msg) + } + s.Require().NotPanics(testFunc, "msgServer.DeleteScope") + s.AssertErrorValue(err, tc.expErr, "error from msgServer.DeleteScope") + s.Assert().Equal(expResp, actResp, "response from msgServer.DeleteScope") + + actEvents := em.Events() + s.AssertEqualEvents(tc.expEvents, actEvents, "events emitted during msgServer.WriteScope") + + // If we were expecting an error and/or we got an error, skip the rest of the checks. + if err != nil || len(tc.expErr) > 0 { + return + } + + _, found := s.app.MetadataKeeper.GetScope(ctx, tc.msg.ScopeId) + s.Assert().False(found, "found bool returned from GetScope(%q) after msgServer.DeleteScope", tc.msg.ScopeId) + + var navs []types.NetAssetValue + err = s.app.MetadataKeeper.IterateNetAssetValues(ctx, tc.msg.ScopeId, func(nav types.NetAssetValue) bool { + navs = append(navs, nav) + return false + }) + s.Require().NoError(err, "error from IterateNetAssetValues(%q) after msgServer.DeleteScope", tc.msg.ScopeId) + s.Assert().Empty(navs, "navs for %s after msgServer.DeleteScope", tc.msg.ScopeId) + }) + } +} func (s *MsgServerTestSuite) TestAddAndDeleteScopeDataAccess() { scopeSpecID := types.ScopeSpecMetadataAddress(uuid.New()) @@ -202,7 +1202,7 @@ func (s *MsgServerTestSuite) TestAddAndDeleteScopeDataAccess() { _, errDel := s.msgServer.DeleteScopeDataAccess(s.ctx, msgDel) require.NoError(t, errDel, "Failed to make DeleteScopeDataAccessRequest call") - scopeB, foundB := s.app.MetadataKeeper.GetScope(s.ctx, scopeA.ScopeId) + scopeB, foundB := s.app.MetadataKeeper.GetScopeWithValueOwner(s.ctx, scopeA.ScopeId) require.Truef(t, foundB, "Scope %s not found after DeleteScopeOwnerRequest call.", scopeA.ScopeId) assert.Equal(t, scopeA.ScopeId, scopeB.ScopeId, "del ScopeId") @@ -225,7 +1225,7 @@ func (s *MsgServerTestSuite) TestAddAndDeleteScopeDataAccess() { _, errAdd := s.msgServer.AddScopeDataAccess(s.ctx, msgAdd) require.NoError(t, errAdd, "Failed to make AddScopeDataAccessRequest call") - scopeC, foundC := s.app.MetadataKeeper.GetScope(s.ctx, scopeA.ScopeId) + scopeC, foundC := s.app.MetadataKeeper.GetScopeWithValueOwner(s.ctx, scopeA.ScopeId) require.Truef(t, foundC, "Scope %s not found after AddScopeOwnerRequest call.", scopeA.ScopeId) assert.Equal(t, scopeA.ScopeId, scopeC.ScopeId, "add ScopeId") @@ -355,8 +1355,9 @@ func (s *MsgServerTestSuite) TestAddAndDeleteScopeOwners() { ContractSpecIds: nil, } - s.app.MetadataKeeper.SetScope(s.ctx, scopeA) + s.Require().NoError(s.app.MetadataKeeper.SetScope(s.ctx, scopeA), "SetScope(scopeA)") s.app.MetadataKeeper.SetScopeSpecification(s.ctx, scopeSpecA) + s.MakeNonWasmAccounts(addrOriginator, addrServicer) msgDel := types.NewMsgDeleteScopeOwnerRequest( scopeA.ScopeId, @@ -367,7 +1368,7 @@ func (s *MsgServerTestSuite) TestAddAndDeleteScopeOwners() { _, errDel := s.msgServer.DeleteScopeOwner(s.ctx, msgDel) require.NoError(t, errDel, "Failed to make DeleteScopeOwnerRequest call") - scopeB, foundB := s.app.MetadataKeeper.GetScope(s.ctx, scopeA.ScopeId) + scopeB, foundB := s.app.MetadataKeeper.GetScopeWithValueOwner(s.ctx, scopeA.ScopeId) require.Truef(t, foundB, "Scope %s not found after DeleteScopeOwnerRequest call.", scopeA.ScopeId) assert.Equal(t, scopeA.ScopeId, scopeB.ScopeId, "del ScopeId") @@ -390,7 +1391,7 @@ func (s *MsgServerTestSuite) TestAddAndDeleteScopeOwners() { _, errAdd := s.msgServer.AddScopeOwner(s.ctx, msgAdd) require.NoError(t, errAdd, "Failed to make DeleteScopeOwnerRequest call") - scopeC, foundC := s.app.MetadataKeeper.GetScope(s.ctx, scopeA.ScopeId) + scopeC, foundC := s.app.MetadataKeeper.GetScopeWithValueOwner(s.ctx, scopeA.ScopeId) require.Truef(t, foundC, "Scope %s not found after AddScopeOwnerRequest call.", scopeA.ScopeId) assert.Equal(t, scopeA.ScopeId, scopeC.ScopeId, "add ScopeId") @@ -402,43 +1403,44 @@ func (s *MsgServerTestSuite) TestAddAndDeleteScopeOwners() { } func (s *MsgServerTestSuite) TestUpdateValueOwners() { - scopeID1 := types.ScopeMetadataAddress(uuid.New()) - scopeID2 := types.ScopeMetadataAddress(uuid.New()) - scopeIDNotFound := types.ScopeMetadataAddress(uuid.New()) - - scopeID3Diff1 := types.ScopeMetadataAddress(uuid.New()) - scopeID3Diff2 := types.ScopeMetadataAddress(uuid.New()) - scopeID3Diff3 := types.ScopeMetadataAddress(uuid.New()) - scopeID3Same1 := types.ScopeMetadataAddress(uuid.New()) - scopeID3Same2 := types.ScopeMetadataAddress(uuid.New()) - scopeID3Same3 := types.ScopeMetadataAddress(uuid.New()) - - owner1 := sdk.AccAddress("owner1______________").String() - owner2 := sdk.AccAddress("owner2______________").String() - owner3Diff1 := sdk.AccAddress("owner3Diff1_________").String() - owner3Diff2 := sdk.AccAddress("owner3Diff2_________").String() - owner3Diff3 := sdk.AccAddress("owner3Diff3_________").String() - owner3Same1 := sdk.AccAddress("owner3Same1_________").String() - owner3Same2 := sdk.AccAddress("owner3Same2_________").String() - owner3Same3 := sdk.AccAddress("owner3Same3_________").String() - - dataAccess1 := sdk.AccAddress("dataAccess1_________").String() - dataAccess2 := sdk.AccAddress("dataAccess2_________").String() - dataAccess3Diff1 := sdk.AccAddress("dataAccess3Diff1____").String() - dataAccess3Diff2 := sdk.AccAddress("dataAccess3Diff2____").String() - dataAccess3Diff3 := sdk.AccAddress("dataAccess3Diff3____").String() - dataAccess3Same1 := sdk.AccAddress("dataAccess3Same1____").String() - dataAccess3Same2 := sdk.AccAddress("dataAccess3Same2____").String() - dataAccess3Same3 := sdk.AccAddress("dataAccess3Same3____").String() - - valueOwner1 := sdk.AccAddress("valueOwner1_________").String() - valueOwner2 := sdk.AccAddress("valueOwner2_________").String() - valueOwner3Diff1 := sdk.AccAddress("valueOwner3Diff1____").String() - valueOwner3Diff2 := sdk.AccAddress("valueOwner3Diff2____").String() - valueOwner3Diff3 := sdk.AccAddress("valueOwner3Diff3____").String() - valueOwner3Same := sdk.AccAddress("valueOwner3Same_____").String() - - scopeSpecID := types.ScopeSpecMetadataAddress(uuid.New()) + scopeID1 := types.ScopeMetadataAddress(s.newUUID("scope", 1)) // scope1qqc47umrdacx2h6lta047h6lta0sfyvr90 + scopeID2 := types.ScopeMetadataAddress(s.newUUID("scope", 2)) // scope1qqe97umrdacx2h6lta047h6lta0sk6uj0g + scopeIDNotFound := types.ScopeMetadataAddress(s.newUUID("notfound", 0)) // scope1qqc97mn0w3nx7atwv3047h6lta0sylrdee + scopeID3Diff1 := types.ScopeMetadataAddress(s.newUUID("scope_3_diff", 1)) // scope1qqc47umrdacx2hentajxjenxta0sp32qg7 + scopeID3Diff2 := types.ScopeMetadataAddress(s.newUUID("scope_3_diff", 2)) // scope1qqe97umrdacx2hentajxjenxta0s7063ze + scopeID3Diff3 := types.ScopeMetadataAddress(s.newUUID("scope_3_diff", 3)) // scope1qqe47umrdacx2hentajxjenxta0szju7u4 + scopeID3Same1 := types.ScopeMetadataAddress(s.newUUID("scope_3_same", 1)) // scope1qqc47umrdacx2hentaekzmt9ta0sc9gw9t + scopeID3Same2 := types.ScopeMetadataAddress(s.newUUID("scope_3_same", 2)) // scope1qqe97umrdacx2hentaekzmt9ta0s8mcl0v + scopeID3Same3 := types.ScopeMetadataAddress(s.newUUID("scope_3_same", 3)) // scope1qqe47umrdacx2hentaekzmt9ta0smx7s3q + scopeID4 := types.ScopeMetadataAddress(s.newUUID("scope", 4)) // scope1qq697umrdacx2h6lta047h6lta0snl0e64 + + owner1 := sdk.AccAddress("owner1______________").String() // cosmos1damkuetjx9047h6lta047h6lta047h6lccgedl + owner2 := sdk.AccAddress("owner2______________").String() // cosmos1damkuetjxf047h6lta047h6lta047h6lsp5nql + owner3Diff1 := sdk.AccAddress("owner3Diff1_________").String() // cosmos1damkuetjxdzxjenxx9047h6lta047h6lx6slvt + owner3Diff2 := sdk.AccAddress("owner3Diff2_________").String() // cosmos1damkuetjxdzxjenxxf047h6lta047h6l955tqt + owner3Diff3 := sdk.AccAddress("owner3Diff3_________").String() // cosmos1damkuetjxdzxjenxxd047h6lta047h6lyf08yt + owner3Same1 := sdk.AccAddress("owner3Same1_________").String() // cosmos1damkuetjxdfkzmt9x9047h6lta047h6l4tvcmn + owner3Same2 := sdk.AccAddress("owner3Same2_________").String() // cosmos1damkuetjxdfkzmt9xf047h6lta047h6lk9gvhn + owner3Same3 := sdk.AccAddress("owner3Same3_________").String() // cosmos1damkuetjxdfkzmt9xd047h6lta047h6lhcnqnn + + dataAccess1 := sdk.AccAddress("dataAccess1_________").String() // cosmos1v3shgc2pvd3k2umnx9047h6lta047h6lvp7hkw + dataAccess2 := sdk.AccAddress("dataAccess2_________").String() // cosmos1v3shgc2pvd3k2umnxf047h6lta047h6l006r6w + dataAccess3Diff1 := sdk.AccAddress("dataAccess3Diff1____").String() // cosmos1v3shgc2pvd3k2umnxdzxjenxx9047h6lfpj2sv + dataAccess3Diff2 := sdk.AccAddress("dataAccess3Diff2____").String() // cosmos1v3shgc2pvd3k2umnxdzxjenxxf047h6lngt850 + dataAccess3Diff3 := sdk.AccAddress("dataAccess3Diff3____").String() // cosmos1v3shgc2pvd3k2umnxdzxjenxxd047h6lz0mm0w + dataAccess3Same1 := sdk.AccAddress("dataAccess3Same1____").String() // cosmos1v3shgc2pvd3k2umnxdfkzmt9x9047h6l84c2fh + dataAccess3Same2 := sdk.AccAddress("dataAccess3Same2____").String() // cosmos1v3shgc2pvd3k2umnxdfkzmt9xf047h6laup8d5 + dataAccess3Same3 := sdk.AccAddress("dataAccess3Same3____").String() // cosmos1v3shgc2pvd3k2umnxdfkzmt9xd047h6lvm3mk4 + + valueOwner1 := sdk.AccAddress("valueOwner1_________").String() // cosmos1weskcat9famkuetjx9047h6lta047h6l7yqwad + valueOwner2 := sdk.AccAddress("valueOwner2_________").String() // cosmos1weskcat9famkuetjxf047h6lta047h6la2y63d + valueOwner3Diff1 := sdk.AccAddress("valueOwner3Diff1____").String() // cosmos1weskcat9famkuetjxdzxjenxx9047h6lmyvnm0 + valueOwner3Diff2 := sdk.AccAddress("valueOwner3Diff2____").String() // cosmos1weskcat9famkuetjxdzxjenxxf047h6lpd47lv + valueOwner3Diff3 := sdk.AccAddress("valueOwner3Diff3____").String() // cosmos1weskcat9famkuetjxdzxjenxxd047h6ls29zyd + valueOwner3Same := sdk.AccAddress("valueOwner3Same_____").String() // cosmos1weskcat9famkuetjxdfkzmt9ta047h6ly5atem + s.MakeNonWasmAccounts(valueOwner1, valueOwner2, valueOwner3Diff1) + + scopeSpecID := types.ScopeSpecMetadataAddress(s.newUUID("scopespec", 1)) // scopespec1qsc47umrdacx2umsv4347h6lta0s56jv59 ns := func(scopeID types.MetadataAddress, owner, dataAccess, valueOwner string) types.Scope { return types.Scope{ ScopeId: scopeID, @@ -452,7 +1454,7 @@ func (s *MsgServerTestSuite) TestUpdateValueOwners() { return scopeIDs } - newValueOwner := sdk.AccAddress("newValueOwner_______").String() + newValueOwner := sdk.AccAddress("newValueOwner_______").String() // cosmos1dejhw4npd36k2nmhdejhyh6lta047h6lvuu8z5 tests := []struct { name string @@ -469,7 +1471,7 @@ func (s *MsgServerTestSuite) TestUpdateValueOwners() { }, scopeIDs: ids(scopeIDNotFound, scopeID1, scopeID2), signers: []string{valueOwner1, valueOwner2}, - expErr: "scope not found with id " + scopeIDNotFound.String() + ": not found", + expErr: "no account address associated with metadata address \"" + scopeIDNotFound.String() + "\": invalid request", }, { name: "scope 2 of 3 not found", @@ -479,7 +1481,7 @@ func (s *MsgServerTestSuite) TestUpdateValueOwners() { }, scopeIDs: ids(scopeID1, scopeIDNotFound, scopeID2), signers: []string{valueOwner1, valueOwner2}, - expErr: "scope not found with id " + scopeIDNotFound.String() + ": not found", + expErr: "no account address associated with metadata address \"" + scopeIDNotFound.String() + "\": invalid request", }, { name: "scope 3 of 3 not found", @@ -489,7 +1491,7 @@ func (s *MsgServerTestSuite) TestUpdateValueOwners() { }, scopeIDs: ids(scopeID1, scopeID2, scopeIDNotFound), signers: []string{valueOwner1, valueOwner2}, - expErr: "scope not found with id " + scopeIDNotFound.String() + ": not found", + expErr: "no account address associated with metadata address \"" + scopeIDNotFound.String() + "\": invalid request", }, { name: "not properly signed", @@ -499,7 +1501,7 @@ func (s *MsgServerTestSuite) TestUpdateValueOwners() { }, scopeIDs: ids(scopeID1, scopeID2), signers: []string{valueOwner1}, - expErr: "missing signature from existing value owner " + valueOwner2 + ": invalid request", + expErr: "missing signature from existing value owner \"" + valueOwner2 + "\": invalid request", }, { name: "1 scope without value owner", @@ -508,7 +1510,7 @@ func (s *MsgServerTestSuite) TestUpdateValueOwners() { }, scopeIDs: ids(scopeID1), signers: []string{owner1}, - expErr: "scope " + scopeID1.String() + " does not yet have a value owner: invalid request", + expErr: "no account address associated with metadata address \"" + scopeID1.String() + "\": invalid request", }, { name: "1 scope updated", @@ -541,29 +1543,61 @@ func (s *MsgServerTestSuite) TestUpdateValueOwners() { signers: []string{valueOwner3Same}, expErr: "", }, + { + name: "three scopes: no signer for third", + starters: []types.Scope{ + ns(scopeID1, owner1, dataAccess1, valueOwner1), + ns(scopeID2, owner1, dataAccess1, valueOwner1), + ns(scopeID4, owner1, dataAccess1, valueOwner2), + }, + scopeIDs: []types.MetadataAddress{scopeID1, scopeID2, scopeID4}, + signers: []string{valueOwner1}, + expErr: "missing signature from existing value owner \"" + valueOwner2 + "\": invalid request", + }, + { + // This test is the same as above except the third scope already has the desired value owner. + name: "three scopes: signer for first two and third already owned by desired", + starters: []types.Scope{ + ns(scopeID1, owner1, dataAccess1, valueOwner1), + ns(scopeID2, owner1, dataAccess1, valueOwner1), + ns(scopeID4, owner1, dataAccess1, newValueOwner), + }, + scopeIDs: []types.MetadataAddress{scopeID1, scopeID2, scopeID4}, + signers: []string{valueOwner1}, + expErr: "scope \"" + scopeID4.String() + "\" already has the proposed value owner \"" + newValueOwner + "\": invalid request", + }, } for _, tc := range tests { s.Run(tc.name, func() { + // Using a CacheContext so that the test cases don't interact. + ctx, _ := s.ctx.CacheContext() for _, scope := range tc.starters { - s.app.MetadataKeeper.SetScope(s.ctx, scope) - defer s.app.MetadataKeeper.RemoveScope(s.ctx, scope.ScopeId) + assertions.RequireNotPanicsNoError(s.T(), func() error { + return s.app.MetadataKeeper.SetScope(ctx, scope) + }, "SetScope") } + msg := types.MsgUpdateValueOwnersRequest{ ScopeIds: tc.scopeIDs, ValueOwnerAddress: newValueOwner, Signers: tc.signers, } - _, err := s.msgServer.UpdateValueOwners(s.ctx, &msg) - if len(tc.expErr) > 0 { - s.Assert().EqualError(err, tc.expErr, "handler(MsgUpdateValueOwnersRequest)") - } else { - s.Require().NoError(err, "handler(MsgUpdateValueOwnersRequest)") + em := sdk.NewEventManager() + ctx = ctx.WithEventManager(em) + var err error + testFunc := func() { + _, err = s.msgServer.UpdateValueOwners(ctx, &msg) + } + s.Require().NotPanics(testFunc, "UpdateValueOwners(%#v)", msg) + s.AssertErrorValue(err, tc.expErr, "error from UpdateValueOwners") + + if err == nil && len(tc.expErr) == 0 { for i, scopeID := range tc.scopeIDs { - scope, found := s.app.MetadataKeeper.GetScope(s.ctx, scopeID) - if s.Assert().True(found, "[%d]GetScope(%s) found bool", i, scopeID) { - s.Assert().Equal(newValueOwner, scope.ValueOwnerAddress, "[%d] updated scope's value owner", i) + actVO, err2 := s.app.MetadataKeeper.GetScopeValueOwner(ctx, scopeID) + if s.Assert().NoError(err2, "[%d]: error from GetScopeValueOwner(%q)", i, scopeID) { + s.Assert().Equal(msg.ValueOwnerAddress, actVO.String(), "[%d]: addr from GetScopeValueOwner(%q)", i, scopeID) } } } @@ -612,7 +1646,7 @@ func (s *MsgServerTestSuite) TestMigrateValueOwner() { Proposed: "doesn't matter", Signers: []string{"who cares"}, }, - expErr: "cannot iterate over invalid value owner \"\": empty address string is not allowed: invalid request", + expErr: "invalid existing address \"\": empty address string is not allowed: invalid request", }, { name: "no scopes", @@ -630,7 +1664,7 @@ func (s *MsgServerTestSuite) TestMigrateValueOwner() { Proposed: addr("not_for_public_use__"), Signers: []string{addr("incorrect_signer____")}, }, - expErr: "missing signature from existing value owner " + addrW1 + ": invalid request", + expErr: "missing signature from existing value owner \"" + addrW1 + "\": invalid request", }, { name: "1 scope updated", @@ -658,12 +1692,11 @@ func (s *MsgServerTestSuite) TestMigrateValueOwner() { if len(tc.expErr) > 0 { s.Assert().EqualError(err, tc.expErr, "Metadata hander(%T)", tc.msg) } else { - if s.Assert().NoError(err, tc.expErr, "Metadata hander(%T)", tc.msg) { - for i, scopeID := range tc.scopeIDs { - scope, found := s.app.MetadataKeeper.GetScope(s.ctx, scopeID) - s.Assert().True(found, "[%d]: GetScope(%q) found boolean", i, scopeID.String()) - actual := scope.ValueOwnerAddress - s.Assert().Equal(tc.msg.Proposed, actual, "[%d] %q value owner after migrate", i, scopeID.String()) + s.Require().NoError(err, tc.expErr, "Metadata hander(%T)", tc.msg) + for i, scopeID := range tc.scopeIDs { + actVO, err2 := s.app.MetadataKeeper.GetScopeValueOwner(s.ctx, scopeID) + if s.Assert().NoError(err2, "[%d]: error from GetScopeValueOwner(%q)", i, scopeID) { + s.Assert().Equal(tc.msg.Proposed, actVO.String(), "[%d]: addr from GetScopeValueOwner(%q)", i, scopeID) } } } @@ -816,8 +1849,7 @@ func (s *MsgServerTestSuite) TestWriteDeleteRecord() { DataAccess: nil, ValueOwnerAddress: "", } - s.app.MetadataKeeper.SetScope(s.ctx, scope) - defer s.app.MetadataKeeper.RemoveScope(s.ctx, scope.ScopeId) + defer WriteTempScope(s.T(), s.app.MetadataKeeper, s.ctx, scope)() session1UUID := uuid.New() session1 := types.Session{ diff --git a/x/metadata/keeper/query_server.go b/x/metadata/keeper/query_server.go index ff32c205a6..e2611424c5 100644 --- a/x/metadata/keeper/query_server.go +++ b/x/metadata/keeper/query_server.go @@ -85,7 +85,7 @@ func (k Keeper) Scope(c context.Context, req *types.ScopeRequest) (*types.ScopeR case scopeAddr.Empty(): scopeAddr = scopeAddr2 case !scopeAddr.Equals(scopeAddr2): - return &retval, sdkerrors.ErrInvalidRequest.Wrapf("session %s is not part of scope %s", recordAddr, scopeAddr) + return &retval, sdkerrors.ErrInvalidRequest.Wrapf("record %s is not part of scope %s", recordAddr, scopeAddr) } } @@ -94,7 +94,7 @@ func (k Keeper) Scope(c context.Context, req *types.ScopeRequest) (*types.ScopeR } ctx := sdk.UnwrapSDKContext(c) - scope, found := k.GetScope(ctx, scopeAddr) + scope, found := k.GetScopeWithValueOwner(ctx, scopeAddr) if found { retval.Scope = types.WrapScope(&scope, !req.ExcludeIdInfo) } else { @@ -159,9 +159,9 @@ func (k Keeper) ScopesAll(c context.Context, req *types.ScopesAllRequest) (*type prefixStore := prefix.NewStore(kvStore, types.ScopeKeyPrefix) pageRes, err := query.Paginate(prefixStore, pageRequest, func(key, value []byte) error { - var scope types.Scope - vErr := scope.Unmarshal(value) + scope, vErr := k.readScopeBz(value) if vErr == nil { + k.PopulateScopeValueOwner(ctx, &scope) retval.Scopes = append(retval.Scopes, types.WrapScope(&scope, incInfo)) return nil } @@ -300,7 +300,7 @@ func (k Keeper) Sessions(c context.Context, req *types.SessionsRequest) (*types. } if req.IncludeScope { - scope, found := k.GetScope(ctx, scopeAddr) + scope, found := k.GetScopeWithValueOwner(ctx, scopeAddr) if found { retval.Scope = types.WrapScope(&scope, !req.ExcludeIdInfo) } else { @@ -485,7 +485,7 @@ func (k Keeper) Records(c context.Context, req *types.RecordsRequest) (*types.Re } if req.IncludeScope { - scope, found := k.GetScope(ctx, scopeAddr) + scope, found := k.GetScopeWithValueOwner(ctx, scopeAddr) if found { retval.Scope = types.WrapScope(&scope, !req.ExcludeIdInfo) } else { @@ -637,25 +637,14 @@ func (k Keeper) ValueOwnership(c context.Context, req *types.ValueOwnershipReque } ctx := sdk.UnwrapSDKContext(c) - store := ctx.KVStore(k.storeKey) - scopeStore := prefix.NewStore(store, types.GetValueOwnerScopeCacheIteratorPrefix(addr)) - pageRes, err := query.Paginate(scopeStore, req.Pagination, func(key, _ []byte) error { - var ma types.MetadataAddress - if mErr := ma.Unmarshal(key); mErr != nil { - return mErr - } - scopeID, sErr := ma.ScopeUUID() - if sErr != nil { - return sErr - } - retval.ScopeUuids = append(retval.ScopeUuids, scopeID.String()) - return nil - }) + var links types.AccMDLinks + links, retval.Pagination, err = k.bankKeeper.GetScopesForValueOwner(ctx, addr, req.Pagination) if err != nil { - return &retval, sdkerrors.ErrInvalidRequest.Wrapf("paginate: %v", err) + return &retval, sdkerrors.ErrInvalidRequest.Wrapf("error collecting results: %v", err) } - retval.Pagination = pageRes + retval.ScopeUuids = links.GetPrimaryUUIDs() + return &retval, nil } @@ -991,7 +980,7 @@ func (k Keeper) GetByAddr(c context.Context, req *types.GetByAddrRequest) (*type } switch hrp { case types.PrefixScope: - scope, found := k.GetScope(ctx, id) + scope, found := k.GetScopeWithValueOwner(ctx, id) if found { retval.Scopes = append(retval.Scopes, &scope) } else { diff --git a/x/metadata/keeper/query_server_test.go b/x/metadata/keeper/query_server_test.go index 4b281d92fc..11616b3781 100644 --- a/x/metadata/keeper/query_server_test.go +++ b/x/metadata/keeper/query_server_test.go @@ -1,8 +1,11 @@ package keeper_test import ( + "bytes" gocontext "context" "fmt" + "slices" + "strings" "testing" "time" @@ -17,11 +20,18 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" simapp "github.com/provenance-io/provenance/app" + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/x/metadata/keeper" "github.com/provenance-io/provenance/x/metadata/types" ) +func TestQuerierTestSuite(t *testing.T) { + suite.Run(t, new(QueryServerTestSuite)) +} + type QueryServerTestSuite struct { suite.Suite @@ -91,84 +101,990 @@ func (s *QueryServerTestSuite) SetupTest() { s.app.AccountKeeper.SetAccount(s.ctx, s.app.AccountKeeper.NewAccountWithAddress(s.ctx, s.user1Addr)) } -func TestQuerierTestSuite(t *testing.T) { - suite.Run(t, new(QueryServerTestSuite)) +// AssertErrorValue is a wrapper on assertions.AssertErrorValue using this suite's current T(). +func (s *QueryServerTestSuite) AssertErrorValue(theError error, expected string, msgAndArgs ...interface{}) bool { + s.T().Helper() + return assertions.AssertErrorValue(s.T(), theError, expected, msgAndArgs...) } -// TODO: Params tests +// AssertEqualPageResponses will assert that the provided PageResponse values are equal +// with friendlier failure messages than if you just used s.Assert()..Equal(expected, actual). +func (s *QueryServerTestSuite) AssertEqualPageResponses(expected, actual *query.PageResponse, fieldName string) bool { + s.T().Helper() + // Check the individual fields first in a friendlier way than when all combined. + pOK := true + if expected != nil && actual != nil { + // Compare the Total as ints because failures on uint64 show the values in hex, which isn't very handy. + pOK = s.Assert().Equal(int(expected.Total), int(actual.Total), fieldName+".Total (as an int)") && pOK + // Compare the NextKey directly. The failure message will list the bytes in multiple formats (int and char). + pOK = s.Assert().Equalf(expected.NextKey, actual.NextKey, fieldName+".NextKey") && pOK + } + // If one of those failed, we can stop now. + if !pOK { + return false + } + // Run the comparison on them as a whole, just to be sure (and handle when one of them is nil). + return s.Assert().Equal(expected, actual, fieldName) +} -func (s *QueryServerTestSuite) TestScopeQuery() { - app, ctx, queryClient, user1, user2, recordName, sessionName := s.app, s.ctx, s.queryClient, s.user1, s.user2, s.recordName, s.sessionName +// dataSetup is a collection metadata entries (scopes, sessions, records) generated for testing. +type dataSetup struct { + // Scopes are each of the scopes in the order they're created. + Scopes []*types.Scope + // Sessions are each of the sessions for each scope in the order they're created. + // E.g. Sessions[0] are all the sessions in Scopes[0]. + Sessions [][]*types.Session + // Records are each of the records for each session for each scope in they order they're created. + // E.g Records[1][0] are all of the records in Sessions[1][0] (which are all in Scopes[1]). + Records [][][]*types.Record + + // ScopeIDs are the scope metadata addresses for each of the scopes. + // E.g. ScopeIDs[0] is equal to Scopes[0].ScopeId. + ScopeIDs []types.MetadataAddress + // ScopeUUIDs are the uuids in the scope metadata addresses for each of the scopes. + // E.g. ScopeUUIDs[0] is the uuid from Scopes[0].ScopeId (which is also ScopeIDs[0]). + ScopeUUIDs []uuid.UUID + // SessionIDs are the session metadata addresses for each of the sessions for each scope. + // E.g. SessionIDs[1][0] is the session metadata address for Sessions[1][0], (which is in Scopes[1]) etc. + SessionIDs [][]types.MetadataAddress + // SessionUUIDs are the secondary uuids for each of the sessions for each scope. + // E.g. SessionIDs[1][0] = SessionMetadataAddress(ScopeUUIDs[1], SessionUUIDs[0]). + SessionUUIDs [][]uuid.UUID + // RecordIDs are the record metadata addresses of each of the records in each session of each scope. + // E.g. RecordIDs[2][1][0] is equal to Records[2][1][0].GetRecordAddress(). + RecordIDs [][][]types.MetadataAddress + // RecordNames are the names of each of the records in each sesson of each scope. + // E.g. RecordNames[2][1][0] is equal to Records[2][1][0].Name. + RecordNames [][][]string + + // AllScopeSessions is all of the sessions for each scope. Each slice is sorted by address. + // e.g. AllScopeSessions[1] is all of the sessions for Scopes[1], sorted the same as in state. + // The secondary index of this field doesn't correlate to any other slices, only the first index. + AllScopeSessions [][]*types.Session + // AllScopeRecords is all of the records for each scope. Each slice is sorted by address. + // e.g. AllScopeRecords[1] is all of the records for Scopes[1], sorted the same as in state. + // The secondary index of this field doesn't correlate to any other slices, only the first index. + AllScopeRecords [][]*types.Record + + // AllScopes is all of the Scopes sorted by address (same order as in state). + // The index in this field doesn't correlate to any other slices. + AllScopes []*types.Scope + // AllSessions is all of the Sessions sorted by address (same order as in state). + // The index in this field doesn't correlate to any other slices. + AllSessions []*types.Session + // AllRecords is all of the records sorted by address (same order as in state). + // The index in this field doesn't correlate to any other slices. + AllRecords []*types.Record +} - testIDs := make([]types.MetadataAddress, 10) - for i := 0; i < 10; i++ { - valueOwner := "" - if i == 5 { - valueOwner = user2 +// createDataSetup will create all the scopes, sessions, and records needed to fill the provided counts. +// len(counts) = number of scopes. +// len(counts[i]) = number of sessions for scope[i]. +// counts[i][j] = number of records for session j (which is part of scope i). +func createDataSetup(counts [][]int) *dataSetup { + newAccAddr := func(i int) string { + return sdk.AccAddress(fmt.Sprintf("%02d_acc_address______", i)).String() + } + newOwners := func(i int) []types.Party { + rv := ownerPartyList(newAccAddr(i)) + if i%2 != 0 { + rv = append(rv, types.Party{Address: newAccAddr(i + 10), Role: 1}) + } + return rv + } + newScope := func(i int) *types.Scope { + rv := &types.Scope{ + ScopeId: types.ScopeMetadataAddress(newTestUUID(i)), + SpecificationId: types.ScopeSpecMetadataAddress(newTestUUID(i + 16)), + Owners: newOwners(i), + DataAccess: []string{newAccAddr(i + 20)}, + ValueOwnerAddress: newAccAddr(i + 40), + RequirePartyRollup: i%2 == 0, + } + if i > 2 { + rv.DataAccess = append(rv.DataAccess, newAccAddr(i+30)) + } + return rv + } + sessionAddr := func(iScope, iSession int) types.MetadataAddress { + return types.SessionMetadataAddress(newTestUUID(iScope), newTestUUID(80+iScope*16+iSession)) + } + cSpecAddr := func(iScope, iSession int) types.MetadataAddress { + return types.ContractSpecMetadataAddress(newTestUUID(160 + iScope*16 + iSession)) + } + newSession := func(iScope, iSession int) *types.Session { + return &types.Session{ + SessionId: sessionAddr(iScope, iSession), + SpecificationId: cSpecAddr(iScope, iSession), + Parties: newOwners(iScope), + Name: fmt.Sprintf("Scope_%02d_TestSession_%02d", iScope, iSession), + } + } + newRecord := func(iScope, iSession, iRecord int) *types.Record { + rName := fmt.Sprintf("Scope_%02d_TestRecord_%02d_%02d", iScope, iSession, iRecord) + id := fmt.Sprintf("%02d_%02d_%02d", iScope, iSession, iRecord) + return &types.Record{ + Name: rName, + SessionId: sessionAddr(iScope, iSession), + Process: types.Process{ + Name: "process_name_" + id, + ProcessId: &types.Process_Hash{Hash: "process_id_hash_" + id}, + Method: "process_method_" + id, + }, + Inputs: []types.RecordInput{ + { + Name: "inputs[0]_name_" + id, + Source: &types.RecordInput_Hash{Hash: "inputs[0]_source_hash_" + id}, + TypeName: "inputs[0]_type_" + id, + Status: types.RecordInputStatus_Record, + }, + }, + Outputs: []types.RecordOutput{ + { + Hash: "outputs[0]_hash_" + id, + Status: types.ResultStatus_RESULT_STATUS_PASS, + }, + }, + SpecificationId: cSpecAddr(iScope, iSession).MustGetAsRecordSpecAddress(rName), } + } - scopeUUID := uuid.New() - testIDs[i] = types.ScopeMetadataAddress(scopeUUID) - ns := types.NewScope(testIDs[i], nil, ownerPartyList(user1), []string{user1}, valueOwner, false) - app.MetadataKeeper.SetScope(ctx, *ns) + rv := &dataSetup{ + Scopes: make([]*types.Scope, len(counts)), + Sessions: make([][]*types.Session, len(counts)), + Records: make([][][]*types.Record, len(counts)), + ScopeIDs: make([]types.MetadataAddress, len(counts)), + ScopeUUIDs: make([]uuid.UUID, len(counts)), + SessionIDs: make([][]types.MetadataAddress, len(counts)), + SessionUUIDs: make([][]uuid.UUID, len(counts)), + RecordIDs: make([][][]types.MetadataAddress, len(counts)), + RecordNames: make([][][]string, len(counts)), + AllScopeSessions: make([][]*types.Session, len(counts)), + AllScopeRecords: make([][]*types.Record, len(counts)), + } - sessionUUID := uuid.New() - sessionID := types.SessionMetadataAddress(scopeUUID, sessionUUID) - sName := fmt.Sprintf("%s%d", sessionName, i) - session := types.NewSession(sName, sessionID, s.cSpecID, ownerPartyList(user1), nil) - app.MetadataKeeper.SetSession(ctx, *session) + for iScope := range rv.Records { + scope := newScope(iScope) + rv.Scopes[iScope] = scope + rv.ScopeIDs[iScope] = scope.ScopeId + rv.ScopeUUIDs[iScope], _ = scope.ScopeId.ScopeUUID() + rv.AllScopes = append(rv.AllScopes, scope) + + sessionCount := len(counts[iScope]) + rv.Sessions[iScope] = make([]*types.Session, sessionCount) + rv.SessionIDs[iScope] = make([]types.MetadataAddress, sessionCount) + rv.SessionUUIDs[iScope] = make([]uuid.UUID, sessionCount) + rv.Records[iScope] = make([][]*types.Record, sessionCount) + rv.RecordIDs[iScope] = make([][]types.MetadataAddress, sessionCount) + rv.RecordNames[iScope] = make([][]string, sessionCount) + + for iSession := range rv.Records[iScope] { + session := newSession(iScope, iSession) + rv.Sessions[iScope][iSession] = session + rv.SessionIDs[iScope][iSession] = session.SessionId + rv.SessionUUIDs[iScope][iSession], _ = session.SessionId.SessionUUID() + rv.AllSessions = append(rv.AllSessions, session) + rv.AllScopeSessions[iScope] = append(rv.AllScopeSessions[iScope], session) + + recordCount := counts[iScope][iSession] + rv.Records[iScope][iSession] = make([]*types.Record, recordCount) + rv.RecordIDs[iScope][iSession] = make([]types.MetadataAddress, recordCount) + rv.RecordNames[iScope][iSession] = make([]string, recordCount) + + for iRecord := range rv.Records[iScope][iSession] { + record := newRecord(iScope, iSession, iRecord) + rv.Records[iScope][iSession][iRecord] = record + rv.RecordIDs[iScope][iSession][iRecord] = record.GetRecordAddress() + rv.RecordNames[iScope][iSession][iRecord] = record.Name + rv.AllRecords = append(rv.AllRecords, record) + rv.AllScopeRecords[iScope] = append(rv.AllScopeRecords[iScope], record) + } + } - rName := fmt.Sprintf("%s%d", recordName, i) - process := types.NewProcess("processname", &types.Process_Hash{Hash: "HASH"}, "process_method") - record := types.NewRecord(rName, sessionID, *process, []types.RecordInput{}, []types.RecordOutput{}, s.recSpecID) - app.MetadataKeeper.SetRecord(ctx, *record) + slices.SortFunc(rv.AllScopeSessions[iScope], compareSessions) + slices.SortFunc(rv.AllScopeRecords[iScope], compareRecords) + } + + slices.SortFunc(rv.AllScopes, compareScopes) + slices.SortFunc(rv.AllSessions, compareSessions) + slices.SortFunc(rv.AllRecords, compareRecords) + + return rv +} + +// identifyScope will return a human-readable string indicating which scope from this dataSetup is in the provided wrapper. +func (d dataSetup) IdentifyScope(w *types.ScopeWrapper) string { + if w == nil { + return fmt.Sprintf("%#v", w) } - scope0UUID, err := testIDs[0].ScopeUUID() - s.NoError(err, "ScopeUUID error") + for i, scope := range d.Scopes { + if w.Scope != nil && w.Scope.ScopeId.Equals(scope.ScopeId) { + return fmt.Sprintf("Scopes[%d]", i) + } + if w.Scope == nil && w.ScopeIdInfo != nil && w.ScopeIdInfo.ScopeId.Equals(scope.ScopeId) { + return fmt.Sprintf("Scopes[%d]", i) + } + } + return fmt.Sprintf("%#v", w) +} - _, err = queryClient.Scope(gocontext.Background(), &types.ScopeRequest{}) - s.EqualError(err, "empty request parameters: invalid request", "empty request error") +// IdentifyScopes will call IdentifyScope on each of the provided scope wrappers. +func (d dataSetup) IdentifyScopes(scopes []*types.ScopeWrapper) []string { + if scopes == nil { + return nil + } + rv := make([]string, len(scopes)) + for i, scope := range scopes { + rv[i] = d.IdentifyScope(scope) + } + return rv +} - _, err = queryClient.Scope(gocontext.Background(), &types.ScopeRequest{ScopeId: "6332c1a4-foo1-bare-895b-invalid65cb6"}) - s.EqualError(err, "could not parse [6332c1a4-foo1-bare-895b-invalid65cb6] into either a scope address (decoding bech32 failed: invalid character not part of charset: 45) or uuid (invalid UUID format): invalid request", "invalid uuid in request error") +// IdentifySession will return a human-readable string indicating which session from this dataSetup is in the provided wrapper. +func (d dataSetup) IdentifySession(w *types.SessionWrapper) string { + if w == nil { + return fmt.Sprintf("%#v", w) + } + for i := range d.Sessions { + for j, session := range d.Sessions[i] { + if w.Session != nil && w.Session.SessionId.Equals(session.SessionId) { + return fmt.Sprintf("Sessions[%d][%d]", i, j) + } + if w.Session == nil && w.SessionIdInfo != nil && w.SessionIdInfo.SessionId.Equals(session.SessionId) { + return fmt.Sprintf("Sessions[%d][%d]", i, j) + } + } + } + if w.Session != nil && len(w.Session.Name) > 0 { + return w.Session.Name + } + return fmt.Sprintf("%#v", w) +} - // TODO: expand this to test new features/failures of the Scope query. +// IdentifySessions will call IdentifySession on each of the provided session wrappers. +func (d dataSetup) IdentifySessions(sessions []*types.SessionWrapper) []string { + if sessions == nil { + return nil + } + rv := make([]string, len(sessions)) + for i, session := range sessions { + rv[i] = d.IdentifySession(session) + } + return rv +} - fullReq0 := types.ScopeRequest{ - ScopeId: scope0UUID.String(), - IncludeSessions: true, - IncludeRecords: true, +// IdentifyRecord will return a human-readable string indicating which record from this dataSetup is in the provided wrapper. +func (d dataSetup) IdentifyRecord(w *types.RecordWrapper) string { + if w == nil { + return fmt.Sprintf("%#v", w) + } + for i := range d.Records { + for j := range d.Records[i] { + for k, record := range d.Records[i][j] { + if w.Record != nil && w.Record.Name == record.Name && w.Record.SessionId.Equals(record.SessionId) { + return fmt.Sprintf("Records[%d][%d][%d]", i, j, k) + } + if w.Record == nil && w.RecordIdInfo != nil && w.RecordIdInfo.RecordId.Equals(record.GetRecordAddress()) { + return fmt.Sprintf("Records[%d][%d][%d]", i, j, k) + } + } + } + } + if w.Record != nil && len(w.Record.Name) > 0 { + return w.Record.Name } - scopeResponse, err := queryClient.Scope(gocontext.Background(), &fullReq0) - s.NoError(err, "valid request error") - s.NotNil(scopeResponse.Scope, "scope in scope response") - s.Equal(testIDs[0], scopeResponse.Scope.Scope.ScopeId, "scopeId") + return fmt.Sprintf("%#v", w) +} - record0Name := fmt.Sprintf("%s%v", recordName, 0) - s.Equal(1, len(scopeResponse.Records), "records count") - s.Equal(record0Name, scopeResponse.Records[0].Record.Name, "record name") +// IdentifyRecords will call IdentifyRecord on each of the provided record wrappers. +func (d dataSetup) IdentifyRecords(records []*types.RecordWrapper) []string { + if records == nil { + return nil + } + rv := make([]string, len(records)) + for i, record := range records { + rv[i] = d.IdentifyRecord(record) + } + return rv +} - session0Name := fmt.Sprintf("%s%v", sessionName, 0) - s.Equal(1, len(scopeResponse.Sessions), "session count") - s.Equal(session0Name, scopeResponse.Sessions[0].Session.Name, "session name") +// newTestUUID creates a new UUID by converting the provided int into a two char +// hex code then repeating those two chars 8 times (for 16 total bytes). +func newTestUUID(i int) uuid.UUID { + return uuid.UUID([]byte(strings.Repeat(fmt.Sprintf("%02X", i), 8))) +} - // only one scope has value owner set (user2) - valueResponse, err := queryClient.ValueOwnership(gocontext.Background(), &types.ValueOwnershipRequest{Address: user2}) - s.NoError(err) - s.Len(valueResponse.ScopeUuids, 1) +// compareScopes is used to sort scopes by their id (which is how they are sorted in state). +// It returns -1 if a < b, or 0 if a == b, or 1 if a > b. +// E.g To test if a > b, you could do if compareRecords(a, b) > 0. +func compareScopes(a, b *types.Scope) int { + var addrA, addrB types.MetadataAddress + if a != nil { + addrA = a.ScopeId + } + if b != nil { + addrB = b.ScopeId + } + return bytes.Compare(addrA, addrB) +} - // 10 entries as all scopes have user1 as data_owner - ownerResponse, err := queryClient.Ownership(gocontext.Background(), &types.OwnershipRequest{Address: user1}) - s.NoError(err) - s.Len(ownerResponse.ScopeUuids, 10) +// compareSessions is used to sort sessions by their id (which is how they are sorted in state). +// It returns -1 if a < b, or 0 if a == b, or 1 if a > b. +// E.g To test if a > b, you could do if compareRecords(a, b) > 0. +func compareSessions(a, b *types.Session) int { + var addrA, addrB types.MetadataAddress + if a != nil { + addrA = a.SessionId + } + if b != nil { + addrB = b.SessionId + } + return bytes.Compare(addrA, addrB) +} - // one entry for user2 (as value owner) - ownerResponse, err = queryClient.Ownership(gocontext.Background(), &types.OwnershipRequest{Address: user2}) - s.NoError(err) - s.Len(ownerResponse.ScopeUuids, 1) +// compareRecords is used to sort records by their id (which is how they are sorted in state). +// It's not all that efficient, but should be fine or small quantities of records. +// It returns -1 if a < b, or 0 if a == b, or 1 if a > b. +// E.g To test if a > b, you could do if compareRecords(a, b) > 0. +func compareRecords(a, b *types.Record) int { + var addrA, addrB types.MetadataAddress + if a != nil { + addrA = a.GetRecordAddress() + } + if b != nil { + addrB = b.GetRecordAddress() + } + return bytes.Compare(addrA, addrB) +} + +func writeData(t *testing.T, ctx sdk.Context, metadataKeeper keeper.Keeper, data *dataSetup) { + for i, scope := range data.Scopes { + assertions.RequireNotPanicsNoError(t, func() error { + return metadataKeeper.SetScope(ctx, *scope) + }, "[%d]: SetScope(%#v)", i, scope) + } + for i := range data.Sessions { + for j, session := range data.Sessions[i] { + require.NotPanics(t, func() { + metadataKeeper.SetSession(ctx, *session) + }, "[%d][%d]: SetSession(%#v)", i, j, session) + } + } + for i := range data.Records { + for j := range data.Records[i] { + for k, record := range data.Records[i][j] { + require.NotPanics(t, func() { + metadataKeeper.SetRecord(ctx, *record) + }, "[%d][%d][%d]: SetRecord(%#v)", i, j, k, record) + } + } + } +} + +// setData will write all the data in the provided dataSetup to state. +func (s *QueryServerTestSuite) setData(data *dataSetup) { + writeData(s.T(), s.ctx, s.app.MetadataKeeper, data) +} + +// createDataSetup will create a dataSetup from the provided emptyRecords and write it to state. +func (s *QueryServerTestSuite) createData(emptyRecords [][]int) *dataSetup { + data := createDataSetup(emptyRecords) + s.setData(data) + return data +} + +// wrapScopes creates a ScopeWrapper with each of the provided scopes. +func wrapScopes(scopes []*types.Scope, includeIDInfo bool) []*types.ScopeWrapper { + if scopes == nil { + return nil + } + rv := make([]*types.ScopeWrapper, len(scopes)) + for i, scope := range scopes { + rv[i] = types.WrapScope(scope, includeIDInfo) + } + return rv +} + +// wrapSessions creates a SessionWrapper with each of the provided sessions. +func wrapSessions(sessions []*types.Session, includeIDInfo bool) []*types.SessionWrapper { + if sessions == nil { + return nil + } + rv := make([]*types.SessionWrapper, len(sessions)) + for i, session := range sessions { + rv[i] = types.WrapSession(session, includeIDInfo) + } + return rv +} + +// wrapRecords creates a RecordWrapper with each of the provided records. +func wrapRecords(records []*types.Record, includeIDInfo bool) []*types.RecordWrapper { + if records == nil { + return nil + } + rv := make([]*types.RecordWrapper, len(records)) + for i, record := range records { + rv[i] = types.WrapRecord(record, includeIDInfo) + } + return rv } -// TODO: ScopesAll tests +// TODO: Params tests + +func (s *QueryServerTestSuite) TestScopeQuery() { + // Make 3 scopes: + // scopes[0]: one session with one record. + // scopes[1]: one session with two records. + // scopes[2]: two sessions, one record and two records. + data := s.createData([][]int{{1}, {2}, {1, 2}}) + // Valid Scopes indexes: [0] [1] [2] + // Valid Session indexes: [0][0] [1][0] [2][0] [2][1] + // Valid Record indexes: [0][0][0] [1][0][0] [1][0][1] [2][0][0] [2][1][0] [2][1][1] + + tests := []struct { + name string + req types.ScopeRequest + expResp *types.ScopeResponse + expErr string + }{ + { + name: "empty request", + req: types.ScopeRequest{}, + expErr: "empty request parameters: invalid request", + }, + { + name: "invalid scope id", + req: types.ScopeRequest{ScopeId: "6332c1a4-foo1-bare-895b-invalid65cb6"}, + expErr: "could not parse [6332c1a4-foo1-bare-895b-invalid65cb6] into either a scope address (decoding bech32 failed: invalid character not part of charset: 45) or uuid (invalid UUID format): invalid request", + }, + { + name: "invalid session", + req: types.ScopeRequest{SessionAddr: "nope"}, + expErr: "could not parse [nope] into a session address: decoding bech32 failed: invalid bech32 string length 4: invalid request", + }, + { + name: "invalid record", + req: types.ScopeRequest{RecordAddr: "alsonope"}, + expErr: "could not parse [alsonope] into a record address: decoding bech32 failed: invalid separator index -1: invalid request", + }, + { + name: "scope uuid and other session", + req: types.ScopeRequest{ + ScopeId: data.ScopeUUIDs[0].String(), + SessionAddr: data.SessionIDs[1][0].String(), + }, + expErr: "session " + data.SessionIDs[1][0].String() + " is not in scope " + data.ScopeIDs[0].String() + ": invalid request", + }, + { + name: "scope addr and other session", + req: types.ScopeRequest{ + ScopeId: data.ScopeIDs[1].String(), + SessionAddr: data.SessionIDs[2][0].String(), + }, + expErr: "session " + data.SessionIDs[2][0].String() + " is not in scope " + + data.ScopeIDs[1].String() + ": invalid request", + }, + { + name: "scope uuid and other record", + req: types.ScopeRequest{ + ScopeId: data.ScopeUUIDs[2].String(), + RecordAddr: data.RecordIDs[0][0][0].String(), + }, + expErr: "record " + data.RecordIDs[0][0][0].String() + " is not part of scope " + + data.ScopeIDs[2].String() + ": invalid request", + }, + { + name: "scope addr and other record", + req: types.ScopeRequest{ + ScopeId: data.ScopeIDs[1].String(), + RecordAddr: data.RecordIDs[2][0][0].String(), + }, + expErr: "record " + data.RecordIDs[2][0][0].String() + " is not part of scope " + + data.ScopeIDs[1].String() + ": invalid request", + }, + { + name: "session and other record", + req: types.ScopeRequest{ + SessionAddr: data.SessionIDs[0][0].String(), + RecordAddr: data.RecordIDs[2][1][1].String(), + }, + expErr: "session " + data.SessionIDs[0][0].String() + " and record " + + data.RecordIDs[2][1][1].String() + " are not associated with the same scope: invalid request", + }, + { + name: "unknown scope uuid", + req: types.ScopeRequest{ScopeId: newTestUUID(21).String()}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScopeNotFound(types.ScopeMetadataAddress(newTestUUID(21))), + }, + }, + { + name: "unknown scope addr", + req: types.ScopeRequest{ScopeId: types.ScopeMetadataAddress(newTestUUID(22)).String()}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScopeNotFound(types.ScopeMetadataAddress(newTestUUID(22))), + }, + }, + { + name: "unknown session", + req: types.ScopeRequest{SessionAddr: types.SessionMetadataAddress(newTestUUID(23), newTestUUID(24)).String()}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScopeNotFound(types.ScopeMetadataAddress(newTestUUID(23))), + }, + }, + { + name: "unknown record", + req: types.ScopeRequest{RecordAddr: types.RecordMetadataAddress(newTestUUID(25), newTestUUID(26).String()).String()}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScopeNotFound(types.ScopeMetadataAddress(newTestUUID(25))), + }, + }, + { + name: "unknown scope: include request", + req: types.ScopeRequest{ + ScopeId: types.ScopeMetadataAddress(newTestUUID(27)).String(), + IncludeRequest: true, + }, + expResp: &types.ScopeResponse{ + Scope: types.WrapScopeNotFound(types.ScopeMetadataAddress(newTestUUID(27))), + Request: &types.ScopeRequest{ + ScopeId: types.ScopeMetadataAddress(newTestUUID(27)).String(), + IncludeRequest: true, + }, + }, + }, + { + name: "just scope: scope uuid", + req: types.ScopeRequest{ScopeId: data.ScopeUUIDs[0].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[0], true)}, + }, + { + name: "just scope: scope addr", + req: types.ScopeRequest{ScopeId: data.ScopeIDs[1].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[1], true)}, + }, + { + name: "just scope: session addr", + req: types.ScopeRequest{SessionAddr: data.SessionIDs[2][0].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[2], true)}, + }, + { + name: "just scope: record addr", + req: types.ScopeRequest{RecordAddr: data.RecordIDs[1][0][1].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[1], true)}, + }, + { + name: "just scope: scope uuid and session", + req: types.ScopeRequest{ScopeId: data.ScopeUUIDs[2].String(), SessionAddr: data.SessionIDs[2][1].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[2], true)}, + }, + { + name: "just scope: scope addr and session", + req: types.ScopeRequest{ScopeId: data.ScopeIDs[0].String(), SessionAddr: data.SessionIDs[0][0].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[0], true)}, + }, + { + name: "just scope: scope uuid and record", + req: types.ScopeRequest{ScopeId: data.ScopeUUIDs[1].String(), RecordAddr: data.RecordIDs[1][0][0].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[1], true)}, + }, + { + name: "just scope: scope addr and record", + req: types.ScopeRequest{ScopeId: data.ScopeIDs[1].String(), RecordAddr: data.RecordIDs[1][0][1].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[1], true)}, + }, + { + name: "just scope: session and record from same session", + req: types.ScopeRequest{SessionAddr: data.SessionIDs[0][0].String(), RecordAddr: data.RecordIDs[0][0][0].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[0], true)}, + }, + { + name: "just scope: session and record from other session but same scope", + req: types.ScopeRequest{SessionAddr: data.SessionIDs[2][0].String(), RecordAddr: data.RecordIDs[2][1][0].String()}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[2], true)}, + }, + { + name: "just scope: scope uuid and session and record", + req: types.ScopeRequest{ + ScopeId: data.ScopeUUIDs[1].String(), + SessionAddr: data.SessionIDs[1][0].String(), + RecordAddr: data.RecordIDs[1][0][1].String(), + }, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[1], true)}, + }, + { + name: "just scope: scope addr and session and record", + req: types.ScopeRequest{ + ScopeId: data.ScopeIDs[2].String(), + SessionAddr: data.SessionIDs[2][1].String(), + RecordAddr: data.RecordIDs[2][1][1].String(), + }, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[2], true)}, + }, + { + name: "with sessions: scope uuid", + req: types.ScopeRequest{ScopeId: data.ScopeUUIDs[0].String(), IncludeSessions: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[0], true), + Sessions: wrapSessions(data.AllScopeSessions[0], true), + }, + }, + { + name: "with sessions: scope addr", + req: types.ScopeRequest{ScopeId: data.ScopeUUIDs[2].String(), IncludeSessions: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[2], true), + Sessions: wrapSessions(data.AllScopeSessions[2], true), + }, + }, + { + name: "with sessions: session", + req: types.ScopeRequest{SessionAddr: data.SessionIDs[1][0].String(), IncludeSessions: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[1], true), + Sessions: wrapSessions(data.AllScopeSessions[1], true), + }, + }, + { + name: "with sessions: record", + req: types.ScopeRequest{RecordAddr: data.RecordIDs[2][0][0].String(), IncludeSessions: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[2], true), + Sessions: wrapSessions(data.AllScopeSessions[2], true), + }, + }, + { + name: "with records: scope uuid", + req: types.ScopeRequest{ScopeId: data.ScopeUUIDs[2].String(), IncludeRecords: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[2], true), + Records: wrapRecords(data.AllScopeRecords[2], true), + }, + }, + { + name: "with records: scope addr", + req: types.ScopeRequest{ScopeId: data.ScopeIDs[1].String(), IncludeRecords: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[1], true), + Records: wrapRecords(data.AllScopeRecords[1], true), + }, + }, + { + name: "with records: session", + req: types.ScopeRequest{SessionAddr: data.SessionIDs[2][0].String(), IncludeRecords: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[2], true), + // Should be all the records in the scope, not just the ones in the provided session. + Records: wrapRecords(data.AllScopeRecords[2], true), + }, + }, + { + name: "with records: record", + req: types.ScopeRequest{RecordAddr: data.RecordIDs[0][0][0].String(), IncludeRecords: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[0], true), + Records: wrapRecords(data.AllScopeRecords[0], true), + }, + }, + { + name: "with request: scope uuid", + req: types.ScopeRequest{ScopeId: data.ScopeUUIDs[0].String(), IncludeRequest: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[0], true), + Request: &types.ScopeRequest{ScopeId: data.ScopeUUIDs[0].String(), IncludeRequest: true}, + }, + }, + { + name: "with request: scope addr", + req: types.ScopeRequest{ScopeId: data.ScopeIDs[1].String(), IncludeRequest: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[1], true), + Request: &types.ScopeRequest{ScopeId: data.ScopeIDs[1].String(), IncludeRequest: true}, + }, + }, + { + name: "with request: session", + req: types.ScopeRequest{SessionAddr: data.SessionIDs[2][0].String(), IncludeRequest: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[2], true), + Request: &types.ScopeRequest{SessionAddr: data.SessionIDs[2][0].String(), IncludeRequest: true}, + }, + }, + { + name: "with request: record", + req: types.ScopeRequest{RecordAddr: data.RecordIDs[2][1][0].String(), IncludeRequest: true}, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[2], true), + Request: &types.ScopeRequest{RecordAddr: data.RecordIDs[2][1][0].String(), IncludeRequest: true}, + }, + }, + { + name: "no id info: scope uuid", + req: types.ScopeRequest{ScopeId: data.ScopeUUIDs[1].String(), ExcludeIdInfo: true}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[1], false)}, + }, + { + name: "no id info: scope addr", + req: types.ScopeRequest{ScopeId: data.ScopeIDs[0].String(), ExcludeIdInfo: true}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[0], false)}, + }, + { + name: "no id info: session", + req: types.ScopeRequest{SessionAddr: data.SessionIDs[1][0].String(), ExcludeIdInfo: true}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[1], false)}, + }, + { + name: "no id info: record", + req: types.ScopeRequest{RecordAddr: data.RecordIDs[2][0][0].String(), ExcludeIdInfo: true}, + expResp: &types.ScopeResponse{Scope: types.WrapScope(data.Scopes[2], false)}, + }, + { + name: "with sessions, records, request and no id info: scope uuid", + req: types.ScopeRequest{ + ScopeId: data.ScopeUUIDs[0].String(), + IncludeSessions: true, + IncludeRecords: true, + ExcludeIdInfo: true, + IncludeRequest: true, + }, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[0], false), + Sessions: wrapSessions(data.AllScopeSessions[0], false), + Records: wrapRecords(data.AllScopeRecords[0], false), + Request: &types.ScopeRequest{ + ScopeId: data.ScopeUUIDs[0].String(), + IncludeSessions: true, + IncludeRecords: true, + ExcludeIdInfo: true, + IncludeRequest: true, + }, + }, + }, + { + name: "with sessions, records, request and no id info: scope addr", + req: types.ScopeRequest{ + ScopeId: data.ScopeIDs[1].String(), + IncludeSessions: true, + IncludeRecords: true, + ExcludeIdInfo: true, + IncludeRequest: true, + }, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[1], false), + Sessions: wrapSessions(data.AllScopeSessions[1], false), + Records: wrapRecords(data.AllScopeRecords[1], false), + Request: &types.ScopeRequest{ + ScopeId: data.ScopeIDs[1].String(), + IncludeSessions: true, + IncludeRecords: true, + ExcludeIdInfo: true, + IncludeRequest: true, + }, + }, + }, + { + name: "with sessions, records, request and no id info: session", + req: types.ScopeRequest{ + SessionAddr: data.SessionIDs[2][0].String(), + IncludeSessions: true, + IncludeRecords: true, + ExcludeIdInfo: true, + IncludeRequest: true, + }, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[2], false), + Sessions: wrapSessions(data.AllScopeSessions[2], false), + Records: wrapRecords(data.AllScopeRecords[2], false), + Request: &types.ScopeRequest{ + SessionAddr: data.SessionIDs[2][0].String(), + IncludeSessions: true, + IncludeRecords: true, + ExcludeIdInfo: true, + IncludeRequest: true, + }, + }, + }, + { + name: "with sessions, records, request and no id info: record", + req: types.ScopeRequest{ + RecordAddr: data.RecordIDs[2][1][0].String(), + IncludeSessions: true, + IncludeRecords: true, + ExcludeIdInfo: true, + IncludeRequest: true, + }, + expResp: &types.ScopeResponse{ + Scope: types.WrapScope(data.Scopes[2], false), + Sessions: wrapSessions(data.AllScopeSessions[2], false), + Records: wrapRecords(data.AllScopeRecords[2], false), + Request: &types.ScopeRequest{ + RecordAddr: data.RecordIDs[2][1][0].String(), + IncludeSessions: true, + IncludeRecords: true, + ExcludeIdInfo: true, + IncludeRequest: true, + }, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var actResp *types.ScopeResponse + var err error + testFunc := func() { + actResp, err = s.queryClient.Scope(gocontext.Background(), &tc.req) + } + s.Require().NotPanics(testFunc, "queryClient.Scope(...)") + s.AssertErrorValue(err, tc.expErr, "error from queryClient.Scope(...)") + if s.Assert().Equal(tc.expResp, actResp, "response from queryClient.Scope(...)") || tc.expResp == nil || actResp == nil { + // If they're not equal and both not nil, I want to run some extra tests to maybe help identify what's wrong. + // But if they're equal, we're all good. And if either is nil, that'll be obvious in the failure message, so we're done. + return + } + + expScopeName := data.IdentifyScope(tc.expResp.Scope) + actScopeName := data.IdentifyScope(actResp.Scope) + if s.Assert().Equal(expScopeName, actScopeName, "names for response.Scope") { + // If those are equal, just make sure the actual scopes are equal too. + s.Assert().Equal(tc.expResp.Scope, actResp.Scope, "response.Scope") + } + + expSessionNames := data.IdentifySessions(tc.expResp.Sessions) + actSessionNames := data.IdentifySessions(actResp.Sessions) + if s.Assert().Equal(expSessionNames, actSessionNames, "names for response.Sessions") { + // If those are equal, make sure each individual entry is equal too (just to be safe). + for i := range tc.expResp.Sessions { + s.Assert().Equal(tc.expResp.Sessions[i], actResp.Sessions[i], "response.Sessions[%d]", i) + } + } + + expRecordNames := data.IdentifyRecords(tc.expResp.Records) + actRecordNames := data.IdentifyRecords(actResp.Records) + if s.Assert().Equal(expRecordNames, actRecordNames, "names for response.Records") { + for i := range tc.expResp.Records { + s.Assert().Equal(tc.expResp.Records[i], actResp.Records[i], "response.Records[%d]", i) + } + } + + // The request is all strings and bools so it'll look just fine in the failure output. + s.Assert().Equal(tc.expResp.Request, actResp.Request, "response.Request") + }) + } +} + +func (s *QueryServerTestSuite) TestScopesAll() { + // six scopes with various numbers of sessions and records. + data := s.createData([][]int{{1}, {2}, {3}, {2, 1}, {1, 1, 1}, {1, 2}}) + // Valid Scopes indexes (6): [0] [1] [2] [3] [4] [5] + + tests := []struct { + name string + req types.ScopesAllRequest + expResp *types.ScopesAllResponse + expErr string + }{ + { + name: "empty request", + req: types.ScopesAllRequest{}, + expResp: &types.ScopesAllResponse{Scopes: wrapScopes(data.AllScopes, true)}, + }, + { + name: "exclude id info", + req: types.ScopesAllRequest{ExcludeIdInfo: true}, + expResp: &types.ScopesAllResponse{Scopes: wrapScopes(data.AllScopes, false)}, + }, + { + name: "include request", + req: types.ScopesAllRequest{IncludeRequest: true}, + expResp: &types.ScopesAllResponse{ + Scopes: wrapScopes(data.AllScopes, true), + Request: nil, // The test runner will populate this for us. + }, + }, + { + name: "exclude id info and include request", + req: types.ScopesAllRequest{ExcludeIdInfo: true, IncludeRequest: true}, + expResp: &types.ScopesAllResponse{ + Scopes: wrapScopes(data.AllScopes, false), + Request: nil, // The test runner will populate this for us. + }, + }, + { + name: "limit 2, offset 1", + req: types.ScopesAllRequest{Pagination: &query.PageRequest{Offset: 1, Limit: 2}}, + expResp: &types.ScopesAllResponse{ + Scopes: wrapScopes(data.AllScopes[1:3], true), + Pagination: &query.PageResponse{NextKey: data.ScopeUUIDs[3][:]}, + }, + }, + { + name: "next key of 3rd, limit 2", + req: types.ScopesAllRequest{Pagination: &query.PageRequest{Limit: 2, Key: data.ScopeUUIDs[2][:]}}, + expResp: &types.ScopesAllResponse{ + Scopes: wrapScopes(data.AllScopes[2:4], true), + Pagination: &query.PageResponse{NextKey: data.ScopeUUIDs[4][:]}, + }, + }, + { + name: "limit 1, reversed", + req: types.ScopesAllRequest{Pagination: &query.PageRequest{Limit: 1, Reverse: true}}, + expResp: &types.ScopesAllResponse{ + Scopes: wrapScopes(data.AllScopes[5:6], true), + Pagination: &query.PageResponse{NextKey: data.ScopeUUIDs[4][:]}, + }, + }, + { + name: "limit 3, count total, exclude id info, include request", + req: types.ScopesAllRequest{ + ExcludeIdInfo: true, + IncludeRequest: true, + Pagination: &query.PageRequest{Limit: 3, CountTotal: true}, + }, + expResp: &types.ScopesAllResponse{ + Scopes: wrapScopes(data.AllScopes[0:3], false), + Request: nil, // The test runner will populate this for us. + Pagination: &query.PageResponse{Total: 6, NextKey: data.ScopeUUIDs[3][:]}, + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + if tc.req.IncludeRequest && tc.expResp != nil { + tc.expResp.Request = &tc.req + } + if tc.expResp != nil && tc.expResp.Pagination == nil { + tc.expResp.Pagination = &query.PageResponse{} + } + + var actResp *types.ScopesAllResponse + var err error + testFunc := func() { + actResp, err = s.queryClient.ScopesAll(gocontext.Background(), &tc.req) + } + s.Require().NotPanics(testFunc, "queryClient.ScopesAll(...)") + s.AssertErrorValue(err, tc.expErr, "error from queryClient.ScopesAll(...)") + if s.Assert().Equal(tc.expResp, actResp, "response from queryClient.ScopesAll(...)") || tc.expResp == nil || actResp == nil { + // If they're not equal and both not nil, I want to run some extra tests to maybe help identify what's wrong. + // But if they're equal, we're all good. And if either is nil, that'll be obvious in the failure message, so we're done. + return + } + + expScopeNames := data.IdentifyScopes(tc.expResp.Scopes) + actScopeNames := data.IdentifyScopes(actResp.Scopes) + if s.Assert().Equal(expScopeNames, actScopeNames, "names for response.Scopes") { + // If those are equal, make sure each individual entry is equal too (just to be safe). + for i := range tc.expResp.Scopes { + s.Assert().Equal(tc.expResp.Scopes[i], actResp.Scopes[i], "response.Scopes[%d]", i) + } + } + + s.Assert().Equal(tc.expResp.Request, actResp.Request, "response.Request") + s.AssertEqualPageResponses(tc.expResp.Pagination, actResp.Pagination, "response.Pagination") + }) + } +} func (s *QueryServerTestSuite) TestSessionsQuery() { app, ctx, queryClient := s.app, s.ctx, s.queryClient diff --git a/x/metadata/keeper/record.go b/x/metadata/keeper/record.go index fb1d9c4eca..a898435e12 100644 --- a/x/metadata/keeper/record.go +++ b/x/metadata/keeper/record.go @@ -9,6 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" + "github.com/provenance-io/provenance/internal/provutils" "github.com/provenance-io/provenance/x/metadata/types" ) @@ -228,13 +229,13 @@ func (k Keeper) ValidateWriteRecord( inputSpecNames[i] = inputSpec.Name inputSpecMap[inputSpec.Name] = *inputSpec } - missingInputNames := findMissing(inputSpecNames, inputNames) + missingInputNames := provutils.FindMissing(inputSpecNames, inputNames) if len(missingInputNames) > 0 { - return fmt.Errorf("missing input%s %v", pluralEnding(len(missingInputNames)), missingInputNames) + return fmt.Errorf("missing input%s %v", provutils.PluralEnding(missingInputNames), missingInputNames) } - extraInputNames := findMissing(inputNames, inputSpecNames) + extraInputNames := provutils.FindMissing(inputNames, inputSpecNames) if len(extraInputNames) > 0 { - return fmt.Errorf("extra input%s %v", pluralEnding(len(extraInputNames)), extraInputNames) + return fmt.Errorf("extra input%s %v", provutils.PluralEnding(extraInputNames), extraInputNames) } // Make sure all the inputs conform to their spec. diff --git a/x/metadata/keeper/record_test.go b/x/metadata/keeper/record_test.go index 88c8ad910b..85ef935da0 100644 --- a/x/metadata/keeper/record_test.go +++ b/x/metadata/keeper/record_test.go @@ -15,7 +15,6 @@ import ( "github.com/provenance-io/provenance/app" simapp "github.com/provenance-io/provenance/app" - "github.com/provenance-io/provenance/x/metadata/keeper" "github.com/provenance-io/provenance/x/metadata/types" ) @@ -90,7 +89,7 @@ func (s *RecordKeeperTestSuite) SetupTest() { } func (s *RecordKeeperTestSuite) FreshCtx() sdk.Context { - return keeper.AddAuthzCacheToContext(s.app.BaseApp.NewContext(false)) + return FreshCtx(s.app) } func TestRecordKeeperTestSuite(t *testing.T) { diff --git a/x/metadata/keeper/scope.go b/x/metadata/keeper/scope.go index 9fd8ce571b..c3846e1db3 100644 --- a/x/metadata/keeper/scope.go +++ b/x/metadata/keeper/scope.go @@ -1,13 +1,16 @@ package keeper import ( + "errors" "fmt" storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/gogoproto/proto" + "github.com/provenance-io/provenance/internal/provutils" "github.com/provenance-io/provenance/x/metadata/types" ) @@ -17,8 +20,8 @@ func (k Keeper) IterateScopes(ctx sdk.Context, handler func(types.Scope) (stop b it := storetypes.KVStorePrefixIterator(store, types.ScopeKeyPrefix) defer it.Close() for ; it.Valid(); it.Next() { - var scope types.Scope - k.cdc.MustUnmarshal(it.Value(), &scope) + scope := k.mustReadScopeBz(it.Value()) + k.PopulateScopeValueOwner(ctx, &scope) if handler(scope) { break } @@ -44,30 +47,6 @@ func (k Keeper) IterateScopesForAddress(ctx sdk.Context, address sdk.AccAddress, return nil } -// IterateScopesForValueOwner iterates over all scope ids that have the provided value owner. -func (k Keeper) IterateScopesForValueOwner(ctx sdk.Context, valueOwner string, handler func(scopeID types.MetadataAddress) (stop bool)) error { - addr, err := sdk.AccAddressFromBech32(valueOwner) - if err != nil { - return fmt.Errorf("cannot iterate over invalid value owner %q: %w", valueOwner, err) - } - - store := ctx.KVStore(k.storeKey) - prefix := types.GetValueOwnerScopeCacheIteratorPrefix(addr) - it := storetypes.KVStorePrefixIterator(store, prefix) - defer it.Close() - for ; it.Valid(); it.Next() { - var scopeID types.MetadataAddress - if err = scopeID.Unmarshal(it.Key()[len(prefix):]); err != nil { - return err - } - if handler(scopeID) { - break - } - } - - return nil -} - // IterateScopesForScopeSpec processes scopes associated with the provided scope specification id with the given handler. func (k Keeper) IterateScopesForScopeSpec(ctx sdk.Context, scopeSpecID types.MetadataAddress, handler func(scopeID types.MetadataAddress) (stop bool), @@ -88,22 +67,81 @@ func (k Keeper) IterateScopesForScopeSpec(ctx sdk.Context, scopeSpecID types.Met return nil } -// GetScope returns the scope with the given id. -func (k Keeper) GetScope(ctx sdk.Context, id types.MetadataAddress) (scope types.Scope, found bool) { +// GetScope returns the scope with the given id. The ValueOwnerAddress field will always be empty from this method. +// See also: GetScopeWithValueOwner, PopulateScopeValueOwner and GetScopeValueOwner. +func (k Keeper) GetScope(ctx sdk.Context, id types.MetadataAddress) (types.Scope, bool) { if !id.IsScopeAddress() { - return scope, false + return types.Scope{}, false } store := ctx.KVStore(k.storeKey) b := store.Get(id.Bytes()) if b == nil { return types.Scope{}, false } - k.cdc.MustUnmarshal(b, &scope) - return scope, true + return k.mustReadScopeBz(b), true +} + +// readScopeBz will unmarshal the given bytes into a Scope. +// The ValueOwnerAddress will always be empty. See also: GetScopeValueOwner, PopulateScopeValueOwner. +func (k Keeper) readScopeBz(bz []byte) (types.Scope, error) { + var scope types.Scope + err := k.cdc.Unmarshal(bz, &scope) + // In the viridian upgrade, we switched to using the bank module to track the ValueOwnerAddress, + // but we didn't update all the scopes to remove the data from that field. Any value in that field + // in state is therefore not to be trusted (probably out of date), so we clear it out here. + scope.ValueOwnerAddress = "" + return scope, err +} + +// mustReadScopeBz will unmarshal the given bytes into a Scope, panicking if there's an error. +// The ValueOwnerAddress will always be empty. See also: GetScopeValueOwner, PopulateScopeValueOwner. +func (k Keeper) mustReadScopeBz(bz []byte) types.Scope { + scope, err := k.readScopeBz(bz) + if err != nil { + panic(err) + } + return scope +} + +// GetScopeWithValueOwner will get a scope from state and also look up and set its value owner field. +func (k Keeper) GetScopeWithValueOwner(ctx sdk.Context, id types.MetadataAddress) (scope types.Scope, found bool) { + scope, found = k.GetScope(ctx, id) + if found { + k.PopulateScopeValueOwner(ctx, &scope) + } + return scope, found +} + +// PopulateScopeValueOwner will look up and set the ValueOwnerAddress in the provided scope. +func (k Keeper) PopulateScopeValueOwner(ctx sdk.Context, scope *types.Scope) { + vo, err := k.GetScopeValueOwner(ctx, scope.ScopeId) + if err == nil && len(vo) > 0 { + scope.ValueOwnerAddress = vo.String() + } else { + scope.ValueOwnerAddress = "" + } } // SetScope stores a scope in the module kv store. -func (k Keeper) SetScope(ctx sdk.Context, scope types.Scope) { +func (k Keeper) SetScope(ctx sdk.Context, scope types.Scope) error { + // If there's a value owner in the provided scope, update that record then remove it from the + // scope before writing the scope. If it doesn't have a value owner, we don't do anything about it. + // It shouldn't be possible to delete the value owner record once there is one for a scope. + if len(scope.ValueOwnerAddress) > 0 { + err := k.SetScopeValueOwner(ctx, scope.ScopeId, scope.ValueOwnerAddress) + if err != nil { + return fmt.Errorf("could not set value owner: %w", err) + } + scope.ValueOwnerAddress = "" + } + + k.writeScopeToState(ctx, scope) + return nil +} + +// writeScopeToState writes the given scope to state, updates the related indexes, and emits the appropriate events. +// It's split out from SetScope only so that unit tests can write scopes that have something in the value owner field. +func (k Keeper) writeScopeToState(ctx sdk.Context, scope types.Scope) { store := ctx.KVStore(k.storeKey) b := k.cdc.MustMarshal(&scope) @@ -113,11 +151,13 @@ func (k Keeper) SetScope(ctx sdk.Context, scope types.Scope) { if store.Has(scope.ScopeId) { event = types.NewEventScopeUpdated(scope.ScopeId) action = types.TLAction_Updated - if oldScopeBytes := store.Get(scope.ScopeId); oldScopeBytes != nil { - oldScope = &types.Scope{} - if err := k.cdc.Unmarshal(oldScopeBytes, oldScope); err != nil { - k.Logger(ctx).Error("could not unmarshal old scope", "err", err, "scopeId", scope.ScopeId.String(), "oldScopeBytes", oldScopeBytes) - oldScope = nil + if oldScopeBytes := store.Get(scope.ScopeId); len(oldScopeBytes) > 0 { + os, err := k.readScopeBz(oldScopeBytes) + if err != nil { + k.Logger(ctx).Error("could not unmarshal old scope", "err", err, + "scopeId", scope.ScopeId.String(), "oldScopeBytes", oldScopeBytes) + } else { + oldScope = &os } } } @@ -129,63 +169,165 @@ func (k Keeper) SetScope(ctx sdk.Context, scope types.Scope) { } // RemoveScope removes a scope from the module kv store along with all its records and sessions. -func (k Keeper) RemoveScope(ctx sdk.Context, id types.MetadataAddress) { - if !id.IsScopeAddress() { - panic(fmt.Errorf("invalid address, address must be for a scope")) +func (k Keeper) RemoveScope(ctx sdk.Context, id types.MetadataAddress) error { + if err := id.ValidateIsScopeAddress(); err != nil { + return err } - // iterate and remove all records, groups - store := ctx.KVStore(k.storeKey) + // If the scope already does not exist, don't do anything. scope, found := k.GetScope(ctx, id) if !found { - return + return nil } - // Remove all records - prefix, err := id.ScopeRecordIteratorPrefix() - if err != nil { - panic(err) + // Burn the scope's value owner coin. + if err := k.SetScopeValueOwner(ctx, id, ""); err != nil { + return fmt.Errorf("could not remove scope %s value owner: %w", id, err) } + + // Remove all records. The sessions are deleted by RemoveRecord as the last record in each is deleted. + store := ctx.KVStore(k.storeKey) + prefix, _ := id.ScopeRecordIteratorPrefix() // Can't return an error because we know it's a valid scope id. iter := storetypes.KVStorePrefixIterator(store, prefix) defer iter.Close() for ; iter.Valid(); iter.Next() { k.RemoveRecord(ctx, iter.Key()) } - // Sessions will be removed as the last record in each is deleted. - k.indexScope(store, nil, &scope) store.Delete(id) k.EmitEvent(ctx, types.NewEventScopeDeleted(scope.ScopeId)) defer types.GetIncObjFunc(types.TLType_Scope, types.TLAction_Deleted) + return nil } -// SetScopeValueOwners updates the value owner of all the provided scopes and stores each in the kv store. -// -// Contract: Each provided scope must not have been modified from its value as read from state. -// Changing one before providing it to this function can mess up indexing. -func (k Keeper) SetScopeValueOwners(ctx sdk.Context, scopes []*types.Scope, newValueOwner string) { - // Not using SetScope in here to skip the re-reading of each scope. - // It's expected that sometimes there will be quite a few (100+) scopes to update, so this will save on gas. - store := ctx.KVStore(k.storeKey) +// GetScopeValueOwner gets the value owner of a given scope. +func (k Keeper) GetScopeValueOwner(ctx sdk.Context, id types.MetadataAddress) (sdk.AccAddress, error) { + if !id.IsScopeAddress() { + return nil, fmt.Errorf("cannot get value owner for non-scope metadata address %q", id) + } + return k.bankKeeper.DenomOwner(ctx, id.Denom()) +} - for _, oldScope := range scopes { - // Copy the old scope and update/store the copy. - newScope := *oldScope - newScope.ValueOwnerAddress = newValueOwner - b := k.cdc.MustMarshal(&newScope) - store.Set(newScope.ScopeId, b) - k.indexScope(store, &newScope, oldScope) - k.EmitEvent(ctx, types.NewEventScopeUpdated(oldScope.ScopeId)) +// GetScopeValueOwners gets the value owners for each given scope. +// The AccAddr will be nil for any scope that does not have a value owner. +func (k Keeper) GetScopeValueOwners(ctx sdk.Context, ids []types.MetadataAddress) (types.AccMDLinks, error) { + var errs []error + rv := make(types.AccMDLinks, 0, len(ids)) + for _, id := range ids { + addr, err := k.GetScopeValueOwner(ctx, id) + if err != nil { + errs = append(errs, err) + } else { + rv = append(rv, types.NewAccMDLink(addr, id)) + } } - types.GetIncObjFuncN(types.TLType_Scope, types.TLAction_Updated, len(scopes))() + return rv, errors.Join(errs...) +} + +// SetScopeValueOwner updates the value owner of a scope. +// If there's no current value owner, the coin will be minted for the scope. +// If there's no new value owner, the coin will be burned for the scope. +func (k Keeper) SetScopeValueOwner(ctx sdk.Context, scopeID types.MetadataAddress, newValueOwner string) error { + if err := scopeID.ValidateIsScopeAddress(); err != nil { + return err + } + + var toAddr sdk.AccAddress + doBurn := false + if len(newValueOwner) == 0 { + // If there's no new value owner, we'll send everything to the module account so that we can then burn it. + toAddr = k.moduleAddr + doBurn = true + } else { + // Sending to another account, so make sure it's valid and not blocked. + var err error + toAddr, err = sdk.AccAddressFromBech32(newValueOwner) + if err != nil { + return fmt.Errorf("invalid new value owner address %q: %w", newValueOwner, err) + } + if k.bankKeeper.BlockedAddr(toAddr) { + return sdkerrors.ErrUnauthorized.Wrapf("new value owner %q is not allowed to receive funds", newValueOwner) + } + } + + coin := scopeID.Coin() + fromAddr, err := k.bankKeeper.DenomOwner(ctx, coin.Denom) + if err != nil { + return fmt.Errorf("could not get current value owner of %q: %w", scopeID, err) + } + if fromAddr.String() == newValueOwner { + // no change, nothing more to do. + return nil + } + + coins := sdk.Coins{coin} + if len(fromAddr) == 0 { + // If there's no current value owner, we'll mint it and send it from the module account. + fromAddr = k.moduleAddr + if err = k.bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { + return fmt.Errorf("could not mint scope coin %q: %w", coins, err) + } + } + + if err = k.bankKeeper.SendCoins(ctx, fromAddr, toAddr, coins); err != nil { + return fmt.Errorf("could not send scope coin %q from %s to %s: %w", coins, fromAddr, toAddr, err) + } + + if doBurn { + if err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, coins); err != nil { + return fmt.Errorf("could not burn scope coin %q: %w", coins, err) + } + } + + return nil +} + +// SetScopeValueOwners updates the value owner of one or more scopes. +func (k Keeper) SetScopeValueOwners(ctx sdk.Context, links types.AccMDLinks, newValueOwner string) error { + if len(links) == 0 { + return nil + } + + if err := links.ValidateForScopes(); err != nil { + return err + } + + toAddr, err := sdk.AccAddressFromBech32(newValueOwner) + if err != nil { + return fmt.Errorf("invalid new value owner address %q: %w", newValueOwner, err) + } + if k.bankKeeper.BlockedAddr(toAddr) { + return sdkerrors.ErrUnauthorized.Wrapf("new value owner %s is not allowed to receive funds", newValueOwner) + } + + // Identify the addresses and the amounts to send to each. + var fromAddrs []sdk.AccAddress + fromAddrAmts := make(map[string]sdk.Coins) + for _, link := range links { + coin := link.MDAddr.Coin() + cur, seen := fromAddrAmts[string(link.AccAddr)] + if !seen { + fromAddrs = append(fromAddrs, link.AccAddr) + } + fromAddrAmts[string(link.AccAddr)] = cur.Add(coin) + } + + for _, fromAddr := range fromAddrs { + if !toAddr.Equals(fromAddr) { + if err = k.bankKeeper.SendCoins(ctx, fromAddr, toAddr, fromAddrAmts[string(fromAddr)]); err != nil { + return fmt.Errorf("could not send scope coins %q from %s to %s: %w", fromAddrAmts[string(fromAddr)], fromAddr, toAddr, err) + } + } + } + + return nil } // scopeIndexValues is a struct containing the values used to index a scope. type scopeIndexValues struct { ScopeID types.MetadataAddress Addresses []sdk.AccAddress - ValueOwner sdk.AccAddress SpecificationID types.MetadataAddress } @@ -199,11 +341,6 @@ func getScopeIndexValues(scope *types.Scope) *scopeIndexValues { SpecificationID: scope.SpecificationId, } knownAddrs := make(map[string]bool) - if addr, err := sdk.AccAddressFromBech32(scope.ValueOwnerAddress); err == nil { - rv.ValueOwner = addr - rv.Addresses = append(rv.Addresses, addr) - knownAddrs[scope.ValueOwnerAddress] = true - } for _, dataAccess := range scope.DataAccess { if !knownAddrs[dataAccess] { if addr, err := sdk.AccAddressFromBech32(dataAccess); err == nil { @@ -233,12 +370,9 @@ func getMissingScopeIndexValues(required, found *scopeIndexValues) *scopeIndexVa return required } rv.ScopeID = required.ScopeID - rv.Addresses = findMissingComp(required.Addresses, found.Addresses, func(a1 sdk.AccAddress, a2 sdk.AccAddress) bool { + rv.Addresses = provutils.FindMissingFunc(required.Addresses, found.Addresses, func(a1, a2 sdk.AccAddress) bool { return a1.Equals(a2) }) - if !required.ValueOwner.Equals(found.ValueOwner) { - rv.ValueOwner = required.ValueOwner - } if !required.SpecificationID.Equals(found.SpecificationID) { rv.SpecificationID = required.SpecificationID } @@ -254,9 +388,6 @@ func (v scopeIndexValues) IndexKeys() [][]byte { for _, addr := range v.Addresses { rv = append(rv, types.GetAddressScopeCacheKey(addr, v.ScopeID)) } - if len(v.ValueOwner) > 0 { - rv = append(rv, types.GetValueOwnerScopeCacheKey(v.ValueOwner, v.ScopeID)) - } if !v.SpecificationID.Empty() { rv = append(rv, types.GetScopeSpecScopeCacheKey(v.SpecificationID, v.ScopeID)) } @@ -293,38 +424,44 @@ func (k Keeper) indexScope(store storetypes.KVStore, newScope, oldScope *types.S } // ValidateWriteScope checks the current scope and the proposed scope to determine if the proposed changes are valid -// based on the existing state +// based on the existing state. Returns the addresses allowed to act as transfer agents. func (k Keeper) ValidateWriteScope( ctx sdk.Context, - existing *types.Scope, msg *types.MsgWriteScopeRequest, -) error { +) ([]sdk.AccAddress, error) { proposed := msg.Scope if err := proposed.ValidateBasic(); err != nil { - return err + return nil, err } - // IDs must match - if existing != nil { - if !proposed.ScopeId.Equals(existing.ScopeId) { - return fmt.Errorf("cannot update scope identifier. expected %s, got %s", existing.ScopeId, proposed.ScopeId) - } + var existing *types.Scope + if e, found := k.GetScope(ctx, msg.Scope.ScopeId); found { + existing = &e } - if err := proposed.SpecificationId.Validate(); err != nil { - return fmt.Errorf("invalid specification id: %w", err) - } - if !proposed.SpecificationId.IsScopeSpecificationAddress() { - return fmt.Errorf("invalid specification id: is not scope specification id: %s", proposed.SpecificationId) + // If the scope already exists: + // - Lack of a proposed value owner means there is no desired change to it and we don't need to look it up. + // - Presence of a proposed value owner means we need to look up the existing one + // and require them to be a signer iff it's different from the proposed value owner. + var existingVOAddrs []sdk.AccAddress + if existing != nil && len(proposed.ValueOwnerAddress) > 0 { + vo, err := k.GetScopeValueOwner(ctx, proposed.ScopeId) + if err != nil { + return nil, fmt.Errorf("error identifying current value owner of %q: %w", proposed.ScopeId, err) + } + // It is possible for scopes to not have a value owner. + if len(vo) > 0 { + existing.ValueOwnerAddress = vo.String() + existingVOAddrs = append(existingVOAddrs, vo) + } } // Existing owners are not required to sign when the ONLY change is from one value owner to another. - // If the value owner wasn't previously set, and is being set now, existing owners must sign. - // If anything else is changing, the existing owners must sign. - existingValueOwner := "" + // Signatures from existing owners are required if: + // - Anything other than the value owner is changing. + // - There's a proposed value owner and the scope exists, but does not yet have a value owner. onlyChangeIsValueOwner := false - if existing != nil && len(existing.ValueOwnerAddress) > 0 { - existingValueOwner = existing.ValueOwnerAddress + if existing != nil && len(existing.ValueOwnerAddress) > 0 && existing.ValueOwnerAddress != proposed.ValueOwnerAddress { // Make a copy of proposed scope and set its value owner to the existing one. If it then // equals the existing scope, then the only change in proposed is to the value owner field. proposedCopy := proposed @@ -333,19 +470,19 @@ func (k Keeper) ValidateWriteScope( } var err error - var validatedParties []*PartyDetails + var validatedParties []*types.PartyDetails if !onlyChangeIsValueOwner { scopeSpec, found := k.GetScopeSpecification(ctx, proposed.SpecificationId) if !found { - return fmt.Errorf("scope specification %s not found", proposed.SpecificationId) + return nil, fmt.Errorf("scope specification %s not found", proposed.SpecificationId) } if err = validateRolesPresent(proposed.Owners, scopeSpec.PartiesInvolved); err != nil { - return err + return nil, err } - if err = k.validateProvenanceRole(ctx, BuildPartyDetails(nil, proposed.Owners)); err != nil { - return err + if err = k.validateProvenanceRole(ctx, types.BuildPartyDetails(nil, proposed.Owners)); err != nil { + return nil, err } // Make sure everyone has signed. @@ -356,7 +493,7 @@ func (k Keeper) ValidateWriteScope( // - Value owner signer restrictions are applied. if existing != nil && !existing.Equals(proposed) { if validatedParties, err = k.validateAllRequiredSigned(ctx, existing.GetAllOwnerAddresses(), msg); err != nil { - return err + return nil, err } } } else { @@ -369,31 +506,34 @@ func (k Keeper) ValidateWriteScope( // Note: This means that a scope can be initially written without consideration for signers and roles. if existing != nil { if validatedParties, err = k.validateAllRequiredPartiesSigned(ctx, existing.Owners, existing.Owners, scopeSpec.PartiesInvolved, msg); err != nil { - return err + return nil, err } } } } - usedSigners, err := k.ValidateScopeValueOwnerUpdate(ctx, existingValueOwner, proposed.ValueOwnerAddress, msg) + transferAgents, usedSigners, err := k.ValidateScopeValueOwnersSigners(ctx, existingVOAddrs, proposed.ValueOwnerAddress, msg) if err != nil { - return err + return nil, err } - usedSigners.AlsoUse(GetUsedSigners(validatedParties)) - return k.validateSmartContractSigners(ctx, usedSigners, msg) + usedSigners.AlsoUse(types.GetUsedSigners(validatedParties)) + if err = k.validateSmartContractSigners(ctx, usedSigners, msg); err != nil { + return nil, err + } + return transferAgents, nil } // ValidateDeleteScope checks the current scope and the proposed removal scope to determine if the proposed remove is valid // based on the existing state -func (k Keeper) ValidateDeleteScope(ctx sdk.Context, msg *types.MsgDeleteScopeRequest) error { +func (k Keeper) ValidateDeleteScope(ctx sdk.Context, msg *types.MsgDeleteScopeRequest) ([]sdk.AccAddress, error) { scope, found := k.GetScope(ctx, msg.ScopeId) if !found { - return fmt.Errorf("scope not found with id %s", msg.ScopeId) + return nil, fmt.Errorf("scope not found with id %s", msg.ScopeId) } var err error - var validatedParties []*PartyDetails + var validatedParties []*types.PartyDetails // Make sure everyone has signed. if !scope.RequirePartyRollup { @@ -403,7 +543,7 @@ func (k Keeper) ValidateDeleteScope(ctx sdk.Context, msg *types.MsgDeleteScopeRe // - Value owner signer restrictions are applied. // We don't care about the first one here. if validatedParties, err = k.validateAllRequiredSigned(ctx, scope.GetAllOwnerAddresses(), msg); err != nil { - return err + return nil, err } } else { // New: @@ -413,25 +553,40 @@ func (k Keeper) ValidateDeleteScope(ctx sdk.Context, msg *types.MsgDeleteScopeRe // associated party from the existing scope. // - Value owner signer restrictions are applied. // We don't care about that first one, and only care about the roles one if the spec exists. - scopeSpec, found := k.GetScopeSpecification(ctx, scope.SpecificationId) - if !found { + scopeSpec, specFound := k.GetScopeSpecification(ctx, scope.SpecificationId) + if !specFound { if validatedParties, err = k.validateAllRequiredSigned(ctx, types.GetRequiredPartyAddresses(scope.Owners), msg); err != nil { - return err + return nil, err } } else { if validatedParties, err = k.validateAllRequiredPartiesSigned(ctx, scope.Owners, scope.Owners, scopeSpec.PartiesInvolved, msg); err != nil { - return err + return nil, err } } } - usedSigners, err := k.ValidateScopeValueOwnerUpdate(ctx, scope.ValueOwnerAddress, "", msg) + var existingVOAddrs []sdk.AccAddress + vo, err := k.GetScopeValueOwner(ctx, scope.ScopeId) if err != nil { - return err + return nil, fmt.Errorf("error identifying current value owner of %q: %w", scope.ScopeId, err) + } + // It is possible for older scopes to not have a value owner. + if len(vo) > 0 { + scope.ValueOwnerAddress = vo.String() + existingVOAddrs = append(existingVOAddrs, vo) } - usedSigners.AlsoUse(GetUsedSigners(validatedParties)) - return k.validateSmartContractSigners(ctx, usedSigners, msg) + transferAgents, usedSigners, err := k.ValidateScopeValueOwnersSigners(ctx, existingVOAddrs, "", msg) + if err != nil { + return nil, err + } + + usedSigners.AlsoUse(types.GetUsedSigners(validatedParties)) + err = k.validateSmartContractSigners(ctx, usedSigners, msg) + if err != nil { + return nil, err + } + return transferAgents, nil } // ValidateSetScopeAccountData makes sure that the msg signers have proper authority to @@ -448,7 +603,7 @@ func (k Keeper) ValidateSetScopeAccountData(ctx sdk.Context, msg *types.MsgSetAc } var err error - var validatedParties []*PartyDetails + var validatedParties []*types.PartyDetails // This is similar to ValidateDeleteScope, but the value owner isn't considered, // and we expect the scope spec to still exist. @@ -479,7 +634,7 @@ func (k Keeper) ValidateSetScopeAccountData(ctx sdk.Context, msg *types.MsgSetAc } } - return k.validateSmartContractSigners(ctx, GetUsedSigners(validatedParties), msg) + return k.validateSmartContractSigners(ctx, types.GetUsedSigners(validatedParties), msg) } // ValidateAddScopeDataAccess checks the current scope and the proposed @@ -607,7 +762,7 @@ func (k Keeper) ValidateUpdateScopeOwners( if err := validateRolesPresent(proposed.Owners, scopeSpec.PartiesInvolved); err != nil { return err } - if err := k.validateProvenanceRole(ctx, BuildPartyDetails(nil, proposed.Owners)); err != nil { + if err := k.validateProvenanceRole(ctx, types.BuildPartyDetails(nil, proposed.Owners)); err != nil { return err } @@ -633,7 +788,7 @@ func (k Keeper) ValidateUpdateScopeOwners( if err != nil { return err } - if err = k.validateSmartContractSigners(ctx, GetUsedSigners(validatedParties), msg); err != nil { + if err = k.validateSmartContractSigners(ctx, types.GetUsedSigners(validatedParties), msg); err != nil { return err } } @@ -641,40 +796,30 @@ func (k Keeper) ValidateUpdateScopeOwners( return nil } +// ValidateUpdateValueOwners checks that the signer(s) of the provided msg are authorized to change the value owner +// of the scopes in the links provided. Also checks that the provided links are valid. +// Returns the transfer agents available for the SendCoins. func (k Keeper) ValidateUpdateValueOwners( ctx sdk.Context, - scopes []*types.Scope, - newValueOwner string, + links types.AccMDLinks, + proposed string, msg types.MetadataMsg, -) error { - var existingValueOwners []string - knownValueOwners := make(map[string]bool) - - for _, scope := range scopes { - if len(scope.ValueOwnerAddress) == 0 { - return fmt.Errorf("scope %s does not yet have a value owner", scope.ScopeId) - } - if !knownValueOwners[scope.ValueOwnerAddress] { - existingValueOwners = append(existingValueOwners, scope.ValueOwnerAddress) - knownValueOwners[scope.ValueOwnerAddress] = true - } +) ([]sdk.AccAddress, error) { + if len(links) == 0 { + return nil, errors.New("no scopes found") } - - signers := NewSignersWrapper(msg.GetSignerStrs()) - usedSigners, err := k.validateScopeValueOwnerChangeToProposed(ctx, newValueOwner, signers) - if err != nil { - return err + if err := links.ValidateForScopes(); err != nil { + return nil, err } - - for _, existing := range existingValueOwners { - alsoUsedSigners, err := k.validateScopeValueOwnerChangeFromExisting(ctx, existing, signers, msg) - if err != nil { - return err + if ids := links.GetMDAddrsForAccAddr(proposed); len(ids) > 0 { + if len(ids) == 1 { + return nil, fmt.Errorf("scope %q already has the proposed value owner %q", ids[0], proposed) } - usedSigners.AlsoUse(alsoUsedSigners) + return nil, fmt.Errorf("scopes %q already have the proposed value owner %q", ids, proposed) } - return k.validateSmartContractSigners(ctx, usedSigners, msg) + transferAgents, _, err := k.ValidateScopeValueOwnersSigners(ctx, links.GetAccAddrs(), proposed, msg) + return transferAgents, err } // AddSetNetAssetValues adds a set of net asset values to a scope @@ -694,6 +839,35 @@ func (k Keeper) AddSetNetAssetValues(ctx sdk.Context, scopeID types.MetadataAddr return nil } +// GetNetAssetValue gets the NAV record for the given scopeID with the given price denom. +// If it doesn't exist then nil, nil is returned. +func (k Keeper) GetNetAssetValue(ctx sdk.Context, metadataDenom, priceDenom string) (*types.NetAssetValue, error) { + scopeID, err := types.MetadataAddressFromDenom(metadataDenom) + if err != nil { + return nil, fmt.Errorf("could not get metadata address: %w", err) + } + + store := ctx.KVStore(k.storeKey) + key := types.NetAssetValueKey(scopeID, priceDenom) + value := store.Get(key) + if len(value) == 0 { + return nil, nil + } + + var scopeNAV types.NetAssetValue + err = k.cdc.Unmarshal(value, &scopeNAV) + if err != nil { + return nil, fmt.Errorf("could not read nav for %q with price denom %q: %w", scopeID, priceDenom, err) + } + // The Volume will be zero for NAVs written before that field was added. So if it's still 0, + // we switch it to 1 since that's what it was assumed to be before the Volume field was added. + if scopeNAV.Volume < 1 { + scopeNAV.Volume = 1 + } + + return &scopeNAV, nil +} + // SetNetAssetValue adds/updates a net asset value to scope func (k Keeper) SetNetAssetValue(ctx sdk.Context, scopeID types.MetadataAddress, netAssetValue types.NetAssetValue, source string) error { netAssetValue.UpdatedBlockHeight = uint64(ctx.BlockHeight()) diff --git a/x/metadata/keeper/scope_test.go b/x/metadata/keeper/scope_test.go index 2ed635bc29..59ce7bec70 100644 --- a/x/metadata/keeper/scope_test.go +++ b/x/metadata/keeper/scope_test.go @@ -1,8 +1,9 @@ package keeper_test import ( + "bytes" "fmt" - "sort" + "strings" "testing" "github.com/google/uuid" @@ -21,6 +22,7 @@ import ( simapp "github.com/provenance-io/provenance/app" "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/testutil/testlog" markertypes "github.com/provenance-io/provenance/x/marker/types" "github.com/provenance-io/provenance/x/metadata/keeper" "github.com/provenance-io/provenance/x/metadata/types" @@ -88,7 +90,7 @@ func (s *ScopeKeeperTestSuite) SetupTest() { } func (s *ScopeKeeperTestSuite) FreshCtx() sdk.Context { - return keeper.AddAuthzCacheToContext(s.app.BaseApp.NewContext(false)) + return FreshCtx(s.app) } // AssertErrorValue asserts that: @@ -96,7 +98,60 @@ func (s *ScopeKeeperTestSuite) FreshCtx() sdk.Context { // - If errorString is not empty, theError must equal the errorString. func (s *ScopeKeeperTestSuite) AssertErrorValue(theError error, errorString string, msgAndArgs ...interface{}) bool { s.T().Helper() - return AssertErrorValue(s.T(), theError, errorString, msgAndArgs...) + return assertions.AssertErrorValue(s.T(), theError, errorString, msgAndArgs...) +} + +// SwapBankKeeper will set the bank keeper (in the metadata keeper) to the one provided +// and return a function that will set it back to its original value. +// Standard usage: defer s.SwapBankKeeper(tc.bk)() +// That will execute this method to set the bank keeper, then defer the resulting func (to put it back at the end). +func (s *ScopeKeeperTestSuite) SwapBankKeeper(bk keeper.BankKeeper) func() { + orig := s.app.MetadataKeeper.SetBankKeeper(bk) + return func() { + s.app.MetadataKeeper.SetBankKeeper(orig) + } +} + +// SwapAuthzKeeper will set the authz keeper (in the metadata keeper) to the one provided +// and return a function that will set it back to its original value. +// Standard usage: defer s.SwapAuthzKeeper(tc.bk)() +// That will execute this method to set the authz keeper, then defer the resulting func (to put it back at the end). +func (s *ScopeKeeperTestSuite) SwapAuthzKeeper(ak keeper.AuthzKeeper) func() { + orig := s.app.MetadataKeeper.SetAuthzKeeper(ak) + return func() { + s.app.MetadataKeeper.SetAuthzKeeper(orig) + } +} + +// SwapMarkerKeeper will set the marker keeper (in the metadata keeper) to the one provided +// and return a function that will set it back to its original value. +// Standard usage: defer s.SwapMarkerKeeper(tc.bk)() +// That will execute this method to set the marker keeper, then defer the resulting func (to put it back at the end). +func (s *ScopeKeeperTestSuite) SwapMarkerKeeper(mk keeper.MarkerKeeper) func() { + orig := s.app.MetadataKeeper.SetMarkerKeeper(mk) + return func() { + s.app.MetadataKeeper.SetMarkerKeeper(orig) + } +} + +func (s *ScopeKeeperTestSuite) AccAddressFromBech32(addr string, msg string) sdk.AccAddress { + rv, err := sdk.AccAddressFromBech32(addr) + s.Require().NoError(err, "%s sdk.AccAddressFromBech32(%q)", msg, addr) + return rv +} + +// WriteTempScope will call SetScope on the provided scope and return a func that will call RemoveScope for it. +// Standard usage: defer WriteTempScope(s.T(), s.app.MetadataKeeper, ctx, scope)() +// That will execute the SetScope and defer the call to RemoveScope. +func WriteTempScope(t *testing.T, mdKeeper keeper.Keeper, ctx sdk.Context, scope types.Scope) func() { + assertions.RequireNotPanicsNoError(t, func() error { + return mdKeeper.SetScope(ctx, scope) + }, "SetScope") + return func() { + assertions.RequireNotPanicsNoError(t, func() error { + return mdKeeper.RemoveScope(ctx, scope.ScopeId) + }, "RemoveScope") + } } func TestScopeKeeperTestSuite(t *testing.T) { @@ -123,333 +178,1058 @@ func randomUser() testUser { func (s *ScopeKeeperTestSuite) TestMetadataScopeGetSet() { ctx := s.FreshCtx() - scope, found := s.app.MetadataKeeper.GetScope(ctx, s.scopeID) - s.Assert().NotNil(scope) - s.False(found) - - ns := *types.NewScope(s.scopeID, s.scopeSpecID, ownerPartyList(s.user1), []string{s.user1}, s.user1, false) - s.Assert().NotNil(ns) - s.app.MetadataKeeper.SetScope(ctx, ns) - - scope, found = s.app.MetadataKeeper.GetScope(ctx, s.scopeID) - s.Assert().True(found) - s.Assert().NotNil(scope) - - s.app.MetadataKeeper.RemoveScope(ctx, ns.ScopeId) - scope, found = s.app.MetadataKeeper.GetScope(ctx, s.scopeID) - s.Assert().False(found) - s.Assert().NotNil(scope) + theScope := *types.NewScope(s.scopeID, s.scopeSpecID, ownerPartyList(s.user1), []string{s.user1}, s.user1, false) + moduleAddr := authtypes.NewModuleAddress(types.ModuleName).String() // cosmos1g4z8k7hm6hj5fa7s780slnxjvq2dnpgpj2jy0e + eventCoinReceived := func(receiver string, amount sdk.Coin) sdk.Event { + return sdk.NewEvent("coin_received", + sdk.NewAttribute("receiver", receiver), + sdk.NewAttribute("amount", amount.String()), + ) + } + eventCoinSpent := func(spender string, amount sdk.Coin) sdk.Event { + return sdk.NewEvent("coin_spent", + sdk.NewAttribute("spender", spender), + sdk.NewAttribute("amount", amount.String()), + ) + } + eventTransfer := func(sender, recipient string, amount sdk.Coin) sdk.Event { + return sdk.NewEvent("transfer", + sdk.NewAttribute("recipient", recipient), + sdk.NewAttribute("sender", sender), + sdk.NewAttribute("amount", amount.String()), + ) + } + + tests := []struct { + name string + runner func() + }{ + { + name: "before setting scope", + runner: func() { + expScope := types.Scope{} + actScope, found := s.app.MetadataKeeper.GetScope(ctx, theScope.ScopeId) + s.Assert().False(found, "GetScope found") + s.Assert().Equal(expScope, actScope, "GetScope result") + }, + }, + { + name: "set scope", + runner: func() { + // Note: Management of index entries during SetScope is tested in TestScopeIndexing. + ctx = ctx.WithEventManager(sdk.NewEventManager()) + expEvent, err := sdk.TypedEventToEvent(types.NewEventScopeCreated(theScope.ScopeId)) + s.Require().NoError(err, "TypedEventToEvent NewEventScopeCreated") + amt := theScope.ScopeId.Coin() + expEvents := sdk.Events{ + eventCoinReceived(moduleAddr, amt), + sdk.NewEvent("coinbase", + sdk.NewAttribute("minter", moduleAddr), + sdk.NewAttribute("amount", amt.String()), + ), + eventCoinSpent(moduleAddr, amt), + eventCoinReceived(theScope.ValueOwnerAddress, amt), + eventTransfer(moduleAddr, theScope.ValueOwnerAddress, amt), + sdk.NewEvent("message", sdk.NewAttribute("sender", moduleAddr)), + expEvent, + } + + err = s.app.MetadataKeeper.SetScope(ctx, theScope) + s.Require().NoError(err, "SetScope") + actEvents := ctx.EventManager().Events() + assertions.AssertEqualEvents(s.T(), expEvents, actEvents, "events emitted during SetScope new") + }, + }, + { + name: "after setting it", + runner: func() { + expScope := theScope + expScope.ValueOwnerAddress = "" + actScope, found := s.app.MetadataKeeper.GetScope(ctx, theScope.ScopeId) + s.Assert().True(found, "GetScope found") + s.Assert().Equal(expScope, actScope, "GetScope result") + + actValueOwner, err := s.app.MetadataKeeper.GetScopeValueOwner(ctx, theScope.ScopeId) + s.Require().NoError(err, "GetScopeValueOwner error") + s.Assert().Equal(theScope.ValueOwnerAddress, actValueOwner.String(), "GetScopeValueOwner result") + }, + }, + { + name: "update scope", + runner: func() { + // Note: Management of index entries during SetScope is tested in TestScopeIndexing. + ctx = ctx.WithEventManager(sdk.NewEventManager()) + theScope.DataAccess = append(theScope.DataAccess, s.user2) + expEvent, err := sdk.TypedEventToEvent(types.NewEventScopeUpdated(theScope.ScopeId)) + s.Require().NoError(err, "TypedEventToEvent NewEventScopeUpdated") + expEvents := sdk.Events{expEvent} + + err = s.app.MetadataKeeper.SetScope(ctx, theScope) + s.Require().NoError(err, "SetScope") + actEvents := ctx.EventManager().Events() + assertions.AssertEqualEvents(s.T(), expEvents, actEvents, "events emitted during SetScope update") + }, + }, + { + name: "after update", + runner: func() { + expScope := theScope + expScope.ValueOwnerAddress = "" + actScope, found := s.app.MetadataKeeper.GetScope(ctx, theScope.ScopeId) + s.Assert().True(found, "GetScope found") + s.Assert().Equal(expScope, actScope, "GetScope result") + }, + }, + { + name: "update scope value owner", + runner: func() { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + origOwner := theScope.ValueOwnerAddress + theScope.ValueOwnerAddress = s.user2 + expEvent, err := sdk.TypedEventToEvent(types.NewEventScopeUpdated(theScope.ScopeId)) + s.Require().NoError(err, "TypedEventToEvent NewEventScopeUpdated") + amt := theScope.ScopeId.Coin() + expEvents := sdk.Events{ + eventCoinSpent(origOwner, amt), + eventCoinReceived(theScope.ValueOwnerAddress, amt), + eventTransfer(origOwner, theScope.ValueOwnerAddress, amt), + sdk.NewEvent("message", sdk.NewAttribute("sender", origOwner)), + expEvent, + } + + err = s.app.MetadataKeeper.SetScope(ctx, theScope) + s.Require().NoError(err, "SetScope") + actEvents := ctx.EventManager().Events() + assertions.AssertEqualEvents(s.T(), expEvents, actEvents, "events emitted during SetScope update") + }, + }, + { + name: "scope value owner after updating it", + runner: func() { + expScope := theScope + actScope, found := s.app.MetadataKeeper.GetScopeWithValueOwner(ctx, theScope.ScopeId) + s.Assert().True(found, "GetScope found") + s.Assert().Equal(expScope, actScope, "GetScope result") + }, + }, + { + name: "remove scope", + runner: func() { + // Note: Management of index entries during RemoveScope is tested in TestScopeIndexing. + // More detailed tests of RemoveScope is done in various other tests. + ctx = ctx.WithEventManager(sdk.NewEventManager()) + expEvent, err := sdk.TypedEventToEvent(types.NewEventScopeDeleted(theScope.ScopeId)) + s.Require().NoError(err, "TypedEventToEvent NewEventScopeDeleted") + amt := theScope.ScopeId.Coin() + expEvents := sdk.Events{ + eventCoinSpent(theScope.ValueOwnerAddress, amt), + eventCoinReceived(moduleAddr, amt), + eventTransfer(theScope.ValueOwnerAddress, moduleAddr, amt), + sdk.NewEvent("message", sdk.NewAttribute("sender", theScope.ValueOwnerAddress)), + eventCoinSpent(moduleAddr, amt), + sdk.NewEvent("burn", + sdk.NewAttribute("burner", moduleAddr), + sdk.NewAttribute("amount", amt.String()), + ), + expEvent, + } + + err = s.app.MetadataKeeper.RemoveScope(ctx, theScope.ScopeId) + s.Require().NoError(err, "RemoveScope") + actEvents := ctx.EventManager().Events() + assertions.AssertEqualEvents(s.T(), expEvents, actEvents, "events emitted during RemoveScope") + }, + }, + { + name: "after remove scope", + runner: func() { + expScope := types.Scope{} + actScope, found := s.app.MetadataKeeper.GetScope(ctx, theScope.ScopeId) + s.Assert().False(found, "GetScope found") + s.Assert().Equal(expScope, actScope, "GetScope result") + }, + }, + } + + ok := true + for _, tc := range tests { + ok = s.Run(tc.name, func() { + if !ok { + s.T().Skip("Skipping due to previous failure.") + } + s.Require().NotPanics(tc.runner) + }) && ok + } } -func (s *ScopeKeeperTestSuite) TestSetScopeValueOwners() { - // Setup - // Three scopes, each with different value owners. - // 1st has the value owner also in owners. - // 2nd has the value owner also in data access. - // 3rd does not have the value owner in either data access or owners. - // We will call SetScopeValueOwners once to update all three to a new value owner. - // We will then do some state checking to make sure things are as expected. - addrAlsoOwnerAcc := sdk.AccAddress("addrAlsoOwner_______") - addrAlsoDataAccessAcc := sdk.AccAddress("addrAlsoDataAccess__") - addrSoloAcc := sdk.AccAddress("addrSolo____________") - addrAlsoOwner := addrAlsoOwnerAcc.String() - addrAlsoDataAccess := addrAlsoDataAccessAcc.String() - addrSolo := addrSoloAcc.String() - scopeSpecID := types.ScopeSpecMetadataAddress(uuid.New()) - scopeWOwner := types.Scope{ - ScopeId: types.ScopeMetadataAddress(uuid.New()), - SpecificationId: scopeSpecID, - Owners: []types.Party{{Address: addrAlsoOwner, Role: types.PartyType_PARTY_TYPE_OWNER}}, - DataAccess: nil, - ValueOwnerAddress: addrAlsoOwner, +func (s *ScopeKeeperTestSuite) TestGetScopeWithValueOwner() { + noScopeUUID, err := uuid.FromBytes([]byte("1111111111111111")) + s.Require().NoError(err, "uuid.FromBytes([]byte(\"1111111111111111\"))") + noScopeID := types.ScopeMetadataAddress(noScopeUUID) + + okScopeUUID, err := uuid.FromBytes([]byte("2222222222222222")) + s.Require().NoError(err, "uuid.FromBytes([]byte(\"2222222222222222\"))") + okScopeID := types.ScopeMetadataAddress(okScopeUUID) + + okScope := types.Scope{ + ScopeId: okScopeID, + SpecificationId: s.scopeSpecID, + Owners: ownerPartyList(s.user1), + ValueOwnerAddress: "", + RequirePartyRollup: true, } - scopeWDataAccess := types.Scope{ - ScopeId: types.ScopeMetadataAddress(uuid.New()), - SpecificationId: scopeSpecID, - Owners: nil, - DataAccess: []string{addrAlsoDataAccess}, - ValueOwnerAddress: addrAlsoOwner, + + s.app.MetadataKeeper.SetScope(s.FreshCtx(), okScope) + + tests := []struct { + name string + bk *MockBankKeeper + id types.MetadataAddress + expScope types.Scope + expFound bool + }{ + { + name: "no such scope", + id: noScopeID, + expScope: types.Scope{}, + expFound: false, + }, + { + name: "no such scope but has a value owner on record", + bk: NewMockBankKeeper().WithDenomOwnerResult(noScopeID, s.user3Addr), + id: noScopeID, + // This is testing that the ValueOwnerAddress field is not populated in this case. + expScope: types.Scope{}, + expFound: false, + }, + { + name: "scope without value owner", + id: okScopeID, + expScope: okScope, + expFound: true, + }, + { + name: "scope with value owner", + bk: NewMockBankKeeper().WithDenomOwnerResult(okScopeID, s.user3Addr), + id: okScopeID, + expScope: types.Scope{ + ScopeId: okScope.ScopeId, + SpecificationId: okScope.SpecificationId, + Owners: okScope.Owners, + DataAccess: okScope.DataAccess, + ValueOwnerAddress: s.user3, + RequirePartyRollup: okScope.RequirePartyRollup, + }, + expFound: true, + }, } - scopeSolo := types.Scope{ - ScopeId: types.ScopeMetadataAddress(uuid.New()), - SpecificationId: scopeSpecID, - Owners: nil, - DataAccess: nil, - ValueOwnerAddress: addrSolo, + + for _, tc := range tests { + s.Run(tc.name, func() { + if tc.bk == nil { + tc.bk = NewMockBankKeeper() + } + defer s.SwapBankKeeper(tc.bk)() + + ctx := s.FreshCtx() + var actScope types.Scope + var actFound bool + testFunc := func() { + actScope, actFound = s.app.MetadataKeeper.GetScopeWithValueOwner(ctx, tc.id) + } + s.Require().NotPanics(testFunc, "GetScopeWithValueOwner") + s.Assert().Equal(tc.expScope, actScope, "GetScopeWithValueOwner scope") + s.Assert().Equal(tc.expFound, actFound, "GetScopeWithValueOwner found") + }) } +} - ctx := s.FreshCtx() - mdKeeper := s.app.MetadataKeeper - mdKeeper.SetScope(ctx, scopeWOwner) - mdKeeper.SetScope(ctx, scopeWDataAccess) - mdKeeper.SetScope(ctx, scopeSolo) - - // Get a fresh context without any events. - ctx = s.FreshCtx() - - newUpdateEvent := func(scopeID types.MetadataAddress) sdk.Event { - tev := types.NewEventScopeUpdated(scopeID) - event, err := sdk.TypedEventToEvent(tev) - if err != nil { - panic(err) - } - return event +func (s *ScopeKeeperTestSuite) TestPopulateScopeValueOwner() { + tests := []struct { + name string + bk *MockBankKeeper + scope types.Scope + expVO string + }{ + { + name: "error getting value owner", + bk: NewMockBankKeeper().WithDenomOwnerError(s.scopeID, "oops go boom"), + scope: types.Scope{ScopeId: s.scopeID, ValueOwnerAddress: "initialvo"}, + expVO: "", + }, + { + name: "no value owner", + scope: types.Scope{ScopeId: s.scopeID, ValueOwnerAddress: "initialvo"}, + expVO: "", + }, + { + name: "has value owner", + bk: NewMockBankKeeper().WithDenomOwnerResult(s.scopeID, s.user2Addr), + scope: types.Scope{ScopeId: s.scopeID, ValueOwnerAddress: "initialvo"}, + expVO: s.user2, + }, } - scopes := []*types.Scope{&scopeWOwner, &scopeWDataAccess, &scopeSolo} - addrNewValueOwnerAcc := sdk.AccAddress("addrNewValueOwner___") - addrNewValueOwner := addrNewValueOwnerAcc.String() - testFunc := func() { - mdKeeper.SetScopeValueOwners(ctx, scopes, addrNewValueOwner) + for _, tc := range tests { + s.Run(tc.name, func() { + if tc.bk == nil { + tc.bk = NewMockBankKeeper() + } + defer s.SwapBankKeeper(tc.bk)() + + ctx := s.FreshCtx() + testFunc := func() { + s.app.MetadataKeeper.PopulateScopeValueOwner(ctx, &tc.scope) + } + s.Require().NotPanics(testFunc, "PopulateScopeValueOwner") + actVO := tc.scope.ValueOwnerAddress + s.Assert().Equal(tc.expVO, actVO, "ValueOwnerAddress after PopulateScopeValueOwner") + }) } - s.Require().NotPanics(testFunc, "SetScopeValueOwners") +} - s.Run("emitted events", func() { - expectedEvents := sdk.Events{ - newUpdateEvent(scopeWOwner.ScopeId), - newUpdateEvent(scopeWDataAccess.ScopeId), - newUpdateEvent(scopeSolo.ScopeId), - } - events := ctx.EventManager().Events() - s.Assert().Equal(expectedEvents, events, "events emitted during SetScopeValueOwners") - }) +func (s *ScopeKeeperTestSuite) TestGetScopeValueOwner() { + nonScopeErr := func(id string) string { + return "cannot get value owner for non-scope metadata address \"" + id + "\"" + } tests := []struct { - name string - scope *types.Scope - expIndexes [][]byte - expRemIndexes [][]byte + name string + bk *MockBankKeeper + id types.MetadataAddress + expAddr sdk.AccAddress + expErr string + expBKCall bool }{ { - name: "scopeWOwner", - scope: &scopeWOwner, - expIndexes: [][]byte{ - types.GetAddressScopeCacheKey(addrAlsoOwnerAcc, scopeWOwner.ScopeId), - types.GetAddressScopeCacheKey(addrNewValueOwnerAcc, scopeWOwner.ScopeId), - types.GetValueOwnerScopeCacheKey(addrNewValueOwnerAcc, scopeWOwner.ScopeId), - }, - expRemIndexes: [][]byte{ - types.GetValueOwnerScopeCacheKey(addrAlsoOwnerAcc, scopeWOwner.ScopeId), + name: "nil id", + id: nil, + expErr: nonScopeErr(""), + }, + { + name: "empty id", + id: types.MetadataAddress{}, + expErr: nonScopeErr(""), + }, + { + name: "session id", + id: types.SessionMetadataAddress(s.scopeUUID, s.scopeSpecUUID), + expErr: nonScopeErr(types.SessionMetadataAddress(s.scopeUUID, s.scopeSpecUUID).String()), + }, + { + name: "record id", + id: types.RecordMetadataAddress(s.scopeUUID, "justsomerecord"), + expErr: nonScopeErr(types.RecordMetadataAddress(s.scopeUUID, "justsomerecord").String()), + }, + { + name: "scope spec id", + id: s.scopeSpecID, + expErr: nonScopeErr(s.scopeSpecID.String()), + }, + { + name: "contract spec id", + id: types.ContractSpecMetadataAddress(s.scopeUUID), + expErr: nonScopeErr(types.ContractSpecMetadataAddress(s.scopeUUID).String()), + }, + { + name: "record spec id", + id: types.RecordSpecMetadataAddress(s.scopeUUID, "justsomerecord"), + expErr: nonScopeErr(types.RecordSpecMetadataAddress(s.scopeUUID, "justsomerecord").String()), + }, + { + name: "scope id without owner", + id: s.scopeID, + expAddr: nil, + expErr: "", + expBKCall: true, + }, + { + name: "scope id with lookup error", + bk: NewMockBankKeeper().WithDenomOwnerError(s.scopeID, "this error was injected"), + id: s.scopeID, + expErr: "this error was injected", + expBKCall: true, + }, + { + name: "scope id with owner", + bk: NewMockBankKeeper().WithDenomOwnerResult(s.scopeID, s.user1Addr), + id: s.scopeID, + expAddr: s.user1Addr, + expBKCall: true, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + if tc.bk == nil { + tc.bk = NewMockBankKeeper() + } + defer s.SwapBankKeeper(tc.bk)() + + var expBKCalls BankKeeperCalls + if tc.expBKCall { + expBKCalls.DenomOwner = append(expBKCalls.DenomOwner, tc.id.Denom()) + } + + ctx := s.FreshCtx() + var actAddr sdk.AccAddress + var actErr error + testFunc := func() { + actAddr, actErr = s.app.MetadataKeeper.GetScopeValueOwner(ctx, tc.id) + } + s.Require().NotPanics(testFunc, "GetScopeValueOwner") + s.AssertErrorValue(actErr, tc.expErr, "GetScopeValueOwner error") + s.Assert().Equal(tc.expAddr, actAddr, "GetScopeValueOwner address") + tc.bk.AssertCalls(s.T(), expBKCalls) + }) + } +} + +func (s *ScopeKeeperTestSuite) TestGetScopeValueOwners() { + nonScopeErr := func(id string) string { + return "cannot get value owner for non-scope metadata address \"" + id + "\"" + } + joinErrs := func(errs ...string) string { + return strings.Join(errs, "\n") + } + uuids := make([]uuid.UUID, 10) + scopeIDs := make([]types.MetadataAddress, len(uuids)) + for i := range uuids { + bz := []byte(fmt.Sprintf("uuids[%d]________", i)) + var err error + uuids[i], err = uuid.FromBytes(bz) + s.Require().NoError(err, "uuid.FromBytes(%q)", string(bz)) + scopeIDs[i] = types.ScopeMetadataAddress(uuids[i]) + } + + tests := []struct { + name string + bk *MockBankKeeper + ids []types.MetadataAddress + expLinks types.AccMDLinks + expErr string + expDOCalls []string // Expected calls made to DenomOwner. + }{ + { + name: "nil ids", + ids: nil, + expLinks: types.AccMDLinks{}, + }, + { + name: "empty ids", + ids: []types.MetadataAddress{}, + expLinks: types.AccMDLinks{}, + }, + { + name: "one nil id", + ids: []types.MetadataAddress{nil}, + expLinks: types.AccMDLinks{}, + expErr: nonScopeErr(""), + }, + { + name: "one empty id", + ids: []types.MetadataAddress{{}}, + expLinks: types.AccMDLinks{}, + expErr: nonScopeErr(""), + }, + { + name: "one of each non-scope id", + ids: []types.MetadataAddress{ + types.SessionMetadataAddress(uuids[0], uuids[1]), + types.RecordMetadataAddress(uuids[2], "somerecord"), + types.ScopeSpecMetadataAddress(uuids[3]), + types.ContractSpecMetadataAddress(uuids[4]), + types.RecordSpecMetadataAddress(uuids[5], "somerecord"), }, + expLinks: types.AccMDLinks{}, + expErr: joinErrs( + nonScopeErr(types.SessionMetadataAddress(uuids[0], uuids[1]).String()), + nonScopeErr(types.RecordMetadataAddress(uuids[2], "somerecord").String()), + nonScopeErr(types.ScopeSpecMetadataAddress(uuids[3]).String()), + nonScopeErr(types.ContractSpecMetadataAddress(uuids[4]).String()), + nonScopeErr(types.RecordSpecMetadataAddress(uuids[5], "somerecord").String()), + ), }, { - name: "scopeWDataAccess", - scope: &scopeWDataAccess, - expIndexes: [][]byte{ - types.GetAddressScopeCacheKey(addrAlsoDataAccessAcc, scopeWDataAccess.ScopeId), - types.GetAddressScopeCacheKey(addrNewValueOwnerAcc, scopeWDataAccess.ScopeId), - types.GetValueOwnerScopeCacheKey(addrNewValueOwnerAcc, scopeWDataAccess.ScopeId), + name: "one id: no owner", + ids: []types.MetadataAddress{scopeIDs[0]}, + expLinks: types.AccMDLinks{types.NewAccMDLink(nil, scopeIDs[0])}, + expDOCalls: []string{scopeIDs[0].Denom()}, + }, + { + name: "one id: DenomOwner error", + bk: NewMockBankKeeper().WithDenomOwnerError(scopeIDs[1], "something broke yo"), + ids: []types.MetadataAddress{scopeIDs[1]}, + expLinks: types.AccMDLinks{}, + expErr: "something broke yo", + expDOCalls: []string{scopeIDs[1].Denom()}, + }, + { + name: "three ids: errors for all", + bk: NewMockBankKeeper(). + WithDenomOwnerError(scopeIDs[3], "its now on fire"). + WithDenomOwnerError(scopeIDs[6], "small thing go big boom"). + WithDenomOwnerError(scopeIDs[7], "something broke yo"), + ids: []types.MetadataAddress{scopeIDs[7], scopeIDs[3], scopeIDs[6]}, + expLinks: types.AccMDLinks{}, + expErr: joinErrs("something broke yo", "its now on fire", "small thing go big boom"), + expDOCalls: []string{scopeIDs[7].Denom(), scopeIDs[3].Denom(), scopeIDs[6].Denom()}, + }, + { + name: "three ids: same owner", + bk: NewMockBankKeeper(). + WithDenomOwnerResult(scopeIDs[4], s.user1Addr). + WithDenomOwnerResult(scopeIDs[5], s.user1Addr). + WithDenomOwnerResult(scopeIDs[6], s.user1Addr), + ids: []types.MetadataAddress{scopeIDs[4], scopeIDs[5], scopeIDs[6]}, + expLinks: types.AccMDLinks{ + types.NewAccMDLink(s.user1Addr, scopeIDs[4]), + types.NewAccMDLink(s.user1Addr, scopeIDs[5]), + types.NewAccMDLink(s.user1Addr, scopeIDs[6]), }, - expRemIndexes: [][]byte{ - types.GetValueOwnerScopeCacheKey(addrAlsoDataAccessAcc, scopeWOwner.ScopeId), + expDOCalls: []string{scopeIDs[4].Denom(), scopeIDs[5].Denom(), scopeIDs[6].Denom()}, + }, + { + name: "three ids: different owners", + bk: NewMockBankKeeper(). + WithDenomOwnerResult(scopeIDs[0], s.user1Addr). + WithDenomOwnerResult(scopeIDs[9], s.user2Addr). + WithDenomOwnerResult(scopeIDs[4], s.user3Addr), + ids: []types.MetadataAddress{scopeIDs[0], scopeIDs[9], scopeIDs[4]}, + expLinks: types.AccMDLinks{ + types.NewAccMDLink(s.user1Addr, scopeIDs[0]), + types.NewAccMDLink(s.user2Addr, scopeIDs[9]), + types.NewAccMDLink(s.user3Addr, scopeIDs[4]), }, + expDOCalls: []string{scopeIDs[0].Denom(), scopeIDs[9].Denom(), scopeIDs[4].Denom()}, }, { - name: "scopeSolo", - scope: &scopeSolo, - expIndexes: [][]byte{ - types.GetAddressScopeCacheKey(addrNewValueOwnerAcc, scopeSolo.ScopeId), - types.GetValueOwnerScopeCacheKey(addrNewValueOwnerAcc, scopeSolo.ScopeId), + name: "four ids: one non-scope, error from one, one found, one not found", + bk: NewMockBankKeeper(). + WithDenomOwnerResult(scopeIDs[1], s.user3Addr). + WithDenomOwnerError(scopeIDs[2], "oopsie daisy: no worky"), + ids: []types.MetadataAddress{ + types.ContractSpecMetadataAddress(uuids[7]), + scopeIDs[1], scopeIDs[2], scopeIDs[8], }, - expRemIndexes: [][]byte{ - types.GetAddressScopeCacheKey(addrSoloAcc, scopeWDataAccess.ScopeId), - types.GetValueOwnerScopeCacheKey(addrSoloAcc, scopeWOwner.ScopeId), + expLinks: types.AccMDLinks{ + types.NewAccMDLink(s.user3Addr, scopeIDs[1]), + types.NewAccMDLink(nil, scopeIDs[8]), }, + expErr: joinErrs( + nonScopeErr(types.ContractSpecMetadataAddress(uuids[7]).String()), + "oopsie daisy: no worky", + ), + expDOCalls: []string{scopeIDs[1].Denom(), scopeIDs[2].Denom(), scopeIDs[8].Denom()}, }, } for _, tc := range tests { s.Run(tc.name, func() { - ctx = s.FreshCtx() - newScope, found := mdKeeper.GetScope(ctx, tc.scope.ScopeId) - if s.Assert().True(found, "GetScope found") { - s.Assert().Equal(addrNewValueOwner, newScope.ValueOwnerAddress, "stored scope's value owner address") + if tc.bk == nil { + tc.bk = NewMockBankKeeper() } - s.Assert().NotEqual(addrNewValueOwner, tc.scope.ValueOwnerAddress, "old scope's value owner address") + defer s.SwapBankKeeper(tc.bk)() - store := ctx.KVStore(mdKeeper.GetStoreKey()) - for i, exp := range tc.expIndexes { - s.Assert().True(store.Has(exp), "expected index [%d]", i) + expBKCalls := BankKeeperCalls{ + DenomOwner: tc.expDOCalls, } - for i, notExp := range tc.expRemIndexes { - s.Assert().False(store.Has(notExp), "expected index to be removed [%d]", i) + + ctx := s.FreshCtx() + var actLinks types.AccMDLinks + var actErr error + testFunc := func() { + actLinks, actErr = s.app.MetadataKeeper.GetScopeValueOwners(ctx, tc.ids) } + s.Require().NotPanics(testFunc, "GetScopeValueOwners") + s.AssertErrorValue(actErr, tc.expErr, "GetScopeValueOwners error") + s.Assert().Equal(tc.expLinks, actLinks, "GetScopeValueOwners address") + tc.bk.AssertCalls(s.T(), expBKCalls) }) } } -func (s *ScopeKeeperTestSuite) TestMetadataScopeIterator() { - ctx := s.FreshCtx() - for i := 1; i <= 10; i++ { - valueOwner := "" - if i == 5 { - valueOwner = s.user2 - } - ns := types.NewScope(types.ScopeMetadataAddress(uuid.New()), nil, ownerPartyList(s.user1), []string{s.user1}, valueOwner, false) - s.app.MetadataKeeper.SetScope(ctx, *ns) +func (s *ScopeKeeperTestSuite) TestSetScopeValueOwner() { + decodeID := func(id string) types.MetadataAddress { + rv, err := types.MetadataAddressFromBech32(id) + s.Require().NoError(err, "types.MetadataAddressFromBech32(%q)", id) + return rv } - count := 0 - err := s.app.MetadataKeeper.IterateScopes(ctx, func(s types.Scope) (stop bool) { - count++ - return false - }) - s.Require().NoError(err, "IterateScopes") - s.Assert().Equal(10, count, "number of scopes iterated") + scopeIDStr := "scope1qpz0e5p8py55wa9mhckh3qg5qsasjwvmh2" // generated via CLI. + scopeID := decodeID(scopeIDStr) + moduleAddr := authtypes.NewModuleAddress(types.ModuleName) // cosmos1g4z8k7hm6hj5fa7s780slnxjvq2dnpgpj2jy0e + addr1 := sdk.AccAddress("1addr_______________") // cosmos1x9skgerjta047h6lta047h6lta047h6l4429yc + addr2 := sdk.AccAddress("2addr_______________") // cosmos1xfskgerjta047h6lta047h6lta047h6lh0rr9a - count = 0 - err = s.app.MetadataKeeper.IterateScopesForAddress(ctx, s.user1Addr, func(scopeID types.MetadataAddress) (stop bool) { - count++ - s.True(scopeID.IsScopeAddress()) - return false - }) - s.Require().NoError(err, "IterateScopesForAddress user1") - s.Assert().Equal(10, count, "number of scope ids iterated for user1") - - count = 0 - err = s.app.MetadataKeeper.IterateScopesForAddress(ctx, s.user2Addr, func(scopeID types.MetadataAddress) (stop bool) { - count++ - s.True(scopeID.IsScopeAddress()) - return false - }) - s.Require().NoError(err, "IterateScopesForAddress user2") - s.Assert().Equal(1, count, "number of scope ids iterated for user2") + tests := []struct { + name string + bk *MockBankKeeper + curOwner sdk.AccAddress + scopeID types.MetadataAddress + newValueOwner string + expErr string + expCallBA sdk.AccAddress // BA = BlockedAddr + expCallDO bool // DO = Denom Owner + expCallMint bool + expCallSend *SendCoinsCall + expCallBurn bool + }{ + { + name: "nil scope id", + scopeID: nil, + expErr: "invalid scope metadata address MetadataAddress(nil): address is empty", + }, + { + name: "empty scope id", + scopeID: types.MetadataAddress{}, + expErr: "invalid scope metadata address MetadataAddress{}: address is empty", + }, + { + name: "invalid scope id", + scopeID: types.MetadataAddress{types.ScopeKeyPrefix[0], 0x1, 0x2}, + expErr: "invalid scope metadata address MetadataAddress{0x0, 0x1, 0x2}: incorrect address length (expected: 17, actual: 3)", + }, + { + name: "session", + scopeID: decodeID("session1q98duk50zlfyhpv3q7f88uzygyzdfw8hwdk2x3z8s4r009lk5nl6syhyghk"), + expErr: "invalid scope id \"session1q98duk50zlfyhpv3q7f88uzygyzdfw8hwdk2x3z8s4r009lk5nl6syhyghk\": wrong type", + }, + { + name: "record", + scopeID: decodeID("record1q26mxxwwvw2524dt3dpgf95gnhefy9ndhhsmphsxfntx7c8f52vpklgcn7v"), + expErr: "invalid scope id \"record1q26mxxwwvw2524dt3dpgf95gnhefy9ndhhsmphsxfntx7c8f52vpklgcn7v\": wrong type", + }, + { + name: "scope spec", + scopeID: decodeID("scopespec1qnna3wa2v4hy2l9jlklkvvtxjxes7wjq86"), + expErr: "invalid scope id \"scopespec1qnna3wa2v4hy2l9jlklkvvtxjxes7wjq86\": wrong type", + }, + { + name: "contract spec", + scopeID: decodeID("contractspec1qdwlarvm04p5cl4sca0vmzudksss654dk2"), + expErr: "invalid scope id \"contractspec1qdwlarvm04p5cl4sca0vmzudksss654dk2\": wrong type", + }, + { + name: "record spec", + scopeID: decodeID("recspec1qkrgw9lwe3k5gm5rh24kh0nkkkqujayqx92qrkvsezr6dvvyv4jmcw7t5tc"), + expErr: "invalid scope id \"recspec1qkrgw9lwe3k5gm5rh24kh0nkkkqujayqx92qrkvsezr6dvvyv4jmcw7t5tc\": wrong type", + }, + { + name: "invalid new value owner", + scopeID: scopeID, + newValueOwner: "bill", + expErr: "invalid new value owner address \"bill\": decoding bech32 failed: invalid bech32 string length 4", + }, + { + name: "blocked new value owner", + bk: NewMockBankKeeper().WithBlockedAddr(addr1), + scopeID: scopeID, + newValueOwner: addr1.String(), + expErr: fmt.Sprintf("new value owner %q is not allowed to receive funds: unauthorized", addr1.String()), + expCallBA: addr1, + }, + { + name: "error getting current owner", + bk: NewMockBankKeeper().WithDenomOwnerError(scopeID, "not now clark"), + scopeID: scopeID, + expErr: fmt.Sprintf("could not get current value owner of %q: not now clark", scopeIDStr), + expCallDO: true, + }, + { + name: "no current owner to empty new owner", + scopeID: scopeID, + newValueOwner: "", + expErr: "", + expCallDO: true, + }, + { + name: "no current owner to new owner: error minting", + bk: NewMockBankKeeper().WithMintCoinsErrors("not so fresh"), + scopeID: scopeID, + newValueOwner: addr1.String(), + expErr: fmt.Sprintf("could not mint scope coin \"1nft/%s\": not so fresh", scopeIDStr), + expCallBA: addr1, + expCallDO: true, + expCallMint: true, + }, + { + name: "no current owner to new owner: error sending", + bk: NewMockBankKeeper().WithSendCoinsError(moduleAddr, "it is mine now"), + scopeID: scopeID, + newValueOwner: addr1.String(), + expErr: fmt.Sprintf("could not send scope coin \"1nft/%s\" from %s to %s: it is mine now", + scopeIDStr, moduleAddr.String(), addr1.String()), + expCallBA: addr1, + expCallDO: true, + expCallMint: true, + expCallSend: NewSendCoinsCall(moduleAddr, addr1, scopeID.Coins()), + }, + { + name: "no current owner to new owner: okay", + scopeID: scopeID, + newValueOwner: addr1.String(), + expCallBA: addr1, + expCallDO: true, + expCallMint: true, + expCallSend: NewSendCoinsCall(moduleAddr, addr1, scopeID.Coins()), + }, + { + name: "current owner to self", + curOwner: addr1, + scopeID: scopeID, + newValueOwner: addr1.String(), + expErr: "", + expCallBA: addr1, + expCallDO: true, + }, + { + name: "current owner to new owner: error sending", + bk: NewMockBankKeeper().WithSendCoinsError(addr1, "gonna keep this one"), + curOwner: addr1, + scopeID: scopeID, + newValueOwner: addr2.String(), + expErr: fmt.Sprintf("could not send scope coin \"1nft/%s\" from %s to %s: gonna keep this one", + scopeIDStr, addr1.String(), addr2.String()), + expCallBA: addr2, + expCallDO: true, + expCallSend: NewSendCoinsCall(addr1, addr2, scopeID.Coins()), + }, + { + name: "current owner to new owner: okay", + curOwner: addr1, + scopeID: scopeID, + newValueOwner: addr2.String(), + expCallBA: addr2, + expCallDO: true, + expCallSend: NewSendCoinsCall(addr1, addr2, scopeID.Coins()), + }, + { + name: "current owner to empty new owner: error sending", + bk: NewMockBankKeeper().WithSendCoinsError(addr1, "finders keepers"), + curOwner: addr1, + scopeID: scopeID, + newValueOwner: "", + expErr: fmt.Sprintf("could not send scope coin \"1nft/%s\" from %s to %s: finders keepers", + scopeIDStr, addr1.String(), moduleAddr.String()), + expCallDO: true, + expCallSend: NewSendCoinsCall(addr1, moduleAddr, scopeID.Coins()), + }, + { + name: "current owner to empty new owner: error burning", + bk: NewMockBankKeeper().WithBurnCoinsErrors("too wet"), + curOwner: addr1, + scopeID: scopeID, + newValueOwner: "", + expErr: fmt.Sprintf("could not burn scope coin \"1nft/%s\": too wet", scopeIDStr), + expCallDO: true, + expCallSend: NewSendCoinsCall(addr1, moduleAddr, scopeID.Coins()), + expCallBurn: true, + }, + { + name: "current owner to empty new owner: okay", + curOwner: addr1, + scopeID: scopeID, + newValueOwner: "", + expCallDO: true, + expCallSend: NewSendCoinsCall(addr1, moduleAddr, scopeID.Coins()), + expCallBurn: true, + }, + { + name: "no coin yet with an empty value owner", + scopeID: scopeID, + newValueOwner: "", + expCallDO: true, // DenomOwner called, then nothing else happens here. + expCallMint: false, + expCallSend: nil, + expCallBurn: false, + }, + } - count = 0 - err = s.app.MetadataKeeper.IterateScopes(ctx, func(s types.Scope) (stop bool) { - count++ - return count >= 5 - }) - s.Require().NoError(err, "IterateScopes with early stop") - s.Assert().Equal(5, count, "number of scopes iterated with early stop") -} + for _, tc := range tests { + s.Run(tc.name, func() { + // Set up expected bank keeper calls. + expBKCalls := BankKeeperCalls{} + if len(tc.expCallBA) > 0 { + expBKCalls.BlockedAddr = append(expBKCalls.BlockedAddr, tc.expCallBA) + } + if tc.expCallMint { + expBKCalls.MintCoins = append(expBKCalls.MintCoins, NewMintBurnCall(types.ModuleName, tc.scopeID.Coins())) + } + if tc.expCallBurn { + expBKCalls.BurnCoins = append(expBKCalls.BurnCoins, NewMintBurnCall(types.ModuleName, tc.scopeID.Coins())) + } + if tc.expCallSend != nil { + expBKCalls.SendCoins = append(expBKCalls.SendCoins, tc.expCallSend) + } + if tc.expCallDO { + expBKCalls.DenomOwner = append(expBKCalls.DenomOwner, tc.scopeID.Denom()) + } -func (s *ScopeKeeperTestSuite) TestIterateScopesForValueOwner() { - ownerAddr := sdk.AccAddress("ownerAddr___________").String() - valueOwnerW0 := sdk.AccAddress("valueOwnerW0________").String() - valueOwnerW1 := sdk.AccAddress("valueOwnerW1________").String() - valueOwnerW5 := sdk.AccAddress("valueOwnerW5________").String() - valueOwnerWBadIndexAcc := sdk.AccAddress("valueOwnerWBadIndex_") - valueOwnerWBadIndex := valueOwnerWBadIndexAcc.String() + // Set up the mock bank keeper. + if tc.bk == nil { + tc.bk = NewMockBankKeeper() + } + if len(tc.curOwner) > 0 { + tc.bk = tc.bk.WithDenomOwnerResult(tc.scopeID, tc.curOwner) + } + defer s.SwapBankKeeper(tc.bk)() - scopeSpecID := types.ScopeSpecMetadataAddress(uuid.New()) - scopeIDW1 := types.ScopeMetadataAddress(uuid.New()) - scopeIDW51 := types.ScopeMetadataAddress(uuid.New()) - scopeIDW52 := types.ScopeMetadataAddress(uuid.New()) - scopeIDW53 := types.ScopeMetadataAddress(uuid.New()) - scopeIDW54 := types.ScopeMetadataAddress(uuid.New()) - scopeIDW55 := types.ScopeMetadataAddress(uuid.New()) - - scopeIDs5 := []types.MetadataAddress{ - scopeIDW51, scopeIDW52, scopeIDW53, scopeIDW54, scopeIDW55, - } - sort.Slice(scopeIDs5, func(i, j int) bool { - return scopeIDs5[i].Compare(scopeIDs5[j]) < 0 - }) + ctx := s.FreshCtx() + var err error + testFunc := func() { + err = s.app.MetadataKeeper.SetScopeValueOwner(ctx, tc.scopeID, tc.newValueOwner) + } + s.Require().NotPanics(testFunc, "SetScopeValueOwner(%q, %q)", tc.scopeID, tc.newValueOwner) + s.AssertErrorValue(err, tc.expErr, "error from SetScopeValueOwner(%q, %q)", tc.scopeID, tc.newValueOwner) + tc.bk.AssertCalls(s.T(), expBKCalls) + }) + } +} - ctx := s.FreshCtx() - storeScope := func(valueOwner string, scopeID types.MetadataAddress) { - scope := types.Scope{ - ScopeId: scopeID, - SpecificationId: scopeSpecID, - Owners: []types.Party{{Address: ownerAddr, Role: types.PartyType_PARTY_TYPE_OWNER}}, - ValueOwnerAddress: valueOwner, +func (s *ScopeKeeperTestSuite) TestSetScopeValueOwners() { + scopeCoins := func(scopeIDs ...types.MetadataAddress) sdk.Coins { + var rv sdk.Coins + for _, scopeID := range scopeIDs { + rv = rv.Add(scopeID.Coin()) } - s.app.MetadataKeeper.SetScope(ctx, scope) + return rv } + sendCall := func(from, to sdk.AccAddress, scopeIDs ...types.MetadataAddress) *SendCoinsCall { + return &SendCoinsCall{ + FromAddr: from, + ToAddr: to, + Amt: scopeCoins(scopeIDs...), + } + } + newUUID := func(b byte) uuid.UUID { + return uuid.UUID(bytes.Repeat([]byte{b}, 16)) + } + newScopeID := func(b byte) types.MetadataAddress { + return types.ScopeMetadataAddress(newUUID(b)) + } + + scopeID1 := newScopeID('1') // scope1qqcnzvf3xycnzvf3xycnzvf3xycs2xyeyk + scopeID2 := newScopeID('2') // scope1qqeryv3jxgeryv3jxgeryv3jxgeqy48g0a + scopeID3 := newScopeID('3') // scope1qqenxvenxvenxvenxvenxvenxvesqa360g + scopeID4 := newScopeID('4') // scope1qq6rgdp5xs6rgdp5xs6rgdp5xs6qzkf4tk + scopeID5 := newScopeID('5') // scope1qq6n2df4x56n2df4x56n2df4x56sx7l8tr - storeScope(valueOwnerW1, scopeIDW1) - storeScope(valueOwnerW5, scopeIDW51) - storeScope(valueOwnerW5, scopeIDW52) - storeScope(valueOwnerW5, scopeIDW53) - storeScope(valueOwnerW5, scopeIDW54) - storeScope(valueOwnerW5, scopeIDW55) + scopeSpecID := types.ScopeSpecMetadataAddress(newUUID('x')) // scopespec1q3u8s7rc0pu8s7rc0pu8s7rc0puq8g8xl0 - badMetadataAddress := types.ScopeMetadataAddress(uuid.New()) - badMetadataAddress[0] = 186 // = 0xBA - badIndexKey := types.GetValueOwnerScopeCacheKey(valueOwnerWBadIndexAcc, badMetadataAddress) - ctx.KVStore(s.app.MetadataKeeper.GetStoreKey()).Set(badIndexKey, []byte{0x01}) + addr1 := sdk.AccAddress("1addr_______________") // cosmos1x9skgerjta047h6lta047h6lta047h6l4429yc + addr2 := sdk.AccAddress("2addr_______________") // cosmos1xfskgerjta047h6lta047h6lta047h6lh0rr9a + addr3 := sdk.AccAddress("3addr_______________") // cosmos1xdskgerjta047h6lta047h6lta047h6lw7ypa7 + addr4 := sdk.AccAddress("4addr_______________") // cosmos1x3skgerjta047h6lta047h6lta047h6lnj308h tests := []struct { - name string - valueOwner string - stopAfter int - expScopeIDs []types.MetadataAddress - expErr string + name string + bankK *MockBankKeeper + links types.AccMDLinks + newVO string + expErr string + expBlockedCall bool + expSendCalls []*SendCoinsCall }{ { - name: "empty value owner", - valueOwner: "", - expErr: "cannot iterate over invalid value owner \"\": empty address string is not allowed", + name: "nil links", + links: nil, + newVO: addr4.String(), + expErr: "", + }, + { + name: "empty links", + links: make(types.AccMDLinks, 0), + newVO: addr4.String(), + expErr: "", }, { - name: "invalid value owner", - valueOwner: "notanaddress", - expErr: "cannot iterate over invalid value owner \"notanaddress\": decoding bech32 failed: invalid separator index -1", + name: "link without acc address", + links: types.AccMDLinks{types.NewAccMDLink(nil, scopeID1)}, + newVO: addr4.String(), + expErr: "no account address associated with metadata address \"" + scopeID1.String() + "\"", }, { - name: "error unmarshalling scope id", - valueOwner: valueOwnerWBadIndex, - expErr: "invalid metadata address type: 186", + name: "link without md address", + links: types.AccMDLinks{types.NewAccMDLink(addr1, nil)}, + newVO: addr4.String(), + expErr: "invalid scope metadata address MetadataAddress(nil): address is empty", }, { - name: "no scopes", - valueOwner: valueOwnerW0, - expScopeIDs: nil, + name: "link with scope spec md address", + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeSpecID)}, + newVO: addr4.String(), + expErr: "invalid scope id \"" + scopeSpecID.String() + "\": wrong type", }, { - name: "1 scope", - valueOwner: valueOwnerW1, - expScopeIDs: []types.MetadataAddress{scopeIDW1}, + name: "two links with same md address", + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeID1), types.NewAccMDLink(addr2, scopeID1)}, + newVO: addr4.String(), + expErr: "duplicate metadata address \"" + scopeID1.String() + "\" not allowed", }, { - name: "1 scope stop after", - valueOwner: valueOwnerW1, - stopAfter: 1, - expScopeIDs: []types.MetadataAddress{scopeIDW1}, + name: "empty new value owner", + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeID1)}, + newVO: "", + expErr: "invalid new value owner address \"\": empty address string is not allowed", }, { - name: "5 scopes", - valueOwner: valueOwnerW5, - expScopeIDs: scopeIDs5, + name: "invalid new value owner", + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeID1)}, + newVO: "nope", + expErr: "invalid new value owner address \"nope\": decoding bech32 failed: invalid bech32 string length 4", }, { - name: "5 scopes stop after 1", - valueOwner: valueOwnerW5, - stopAfter: 1, - expScopeIDs: scopeIDs5[0:1], + name: "blocked address", + bankK: NewMockBankKeeper().WithBlockedAddr(addr4), + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeID1)}, + newVO: addr4.String(), + expErr: "new value owner " + addr4.String() + " is not allowed to receive funds: unauthorized", + expBlockedCall: true, }, { - name: "5 scopes stop after 3", - valueOwner: valueOwnerW5, - stopAfter: 3, - expScopeIDs: scopeIDs5[0:3], + name: "one link: new value owner is different", + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeID1)}, + newVO: addr4.String(), + expBlockedCall: true, + expSendCalls: []*SendCoinsCall{sendCall(addr1, addr4, scopeID1)}, }, { - name: "5 scopes stop after 5", - valueOwner: valueOwnerW5, - stopAfter: 5, - expScopeIDs: scopeIDs5[0:5], + name: "one link: new value owner is same", + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeID1)}, + newVO: addr1.String(), + expBlockedCall: true, + }, + { + name: "one link: error sending coins", + bankK: NewMockBankKeeper().WithSendCoinsError(addr1, "not a real error"), + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeID1)}, + newVO: addr4.String(), + expErr: "could not send scope coins \"" + scopeCoins(scopeID1).String() + "\" " + + "from " + addr1.String() + " to " + addr4.String() + ": not a real error", + expBlockedCall: true, + expSendCalls: []*SendCoinsCall{sendCall(addr1, addr4, scopeID1)}, + }, + { + name: "two links: same acc addresses", + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeID1), types.NewAccMDLink(addr1, scopeID2)}, + newVO: addr4.String(), + expBlockedCall: true, + expSendCalls: []*SendCoinsCall{sendCall(addr1, addr4, scopeID1, scopeID2)}, + }, + { + name: "two links: different acc addresses", + links: types.AccMDLinks{types.NewAccMDLink(addr1, scopeID1), types.NewAccMDLink(addr2, scopeID2)}, + newVO: addr4.String(), + expBlockedCall: true, + expSendCalls: []*SendCoinsCall{sendCall(addr1, addr4, scopeID1), sendCall(addr2, addr4, scopeID2)}, + }, + { + name: "mix of same and different acc addresses, one is new value owner", + links: types.AccMDLinks{ + types.NewAccMDLink(addr1, scopeID1), + types.NewAccMDLink(addr2, scopeID2), + types.NewAccMDLink(addr1, scopeID3), + types.NewAccMDLink(addr4, scopeID4), + types.NewAccMDLink(addr3, scopeID5), + }, + newVO: addr4.String(), + expBlockedCall: true, + expSendCalls: []*SendCoinsCall{ + sendCall(addr1, addr4, scopeID1, scopeID3), + sendCall(addr2, addr4, scopeID2), + sendCall(addr3, addr4, scopeID5), + }, + }, + { + name: "three links: error sending from second", + bankK: NewMockBankKeeper().WithSendCoinsError(addr2, "fake error is fake"), + links: types.AccMDLinks{ + types.NewAccMDLink(addr1, scopeID1), + types.NewAccMDLink(addr2, scopeID2), + types.NewAccMDLink(addr3, scopeID3), + }, + newVO: addr4.String(), + expErr: "could not send scope coins \"" + scopeCoins(scopeID2).String() + "\" " + + "from " + addr2.String() + " to " + addr4.String() + ": fake error is fake", + expBlockedCall: true, + expSendCalls: []*SendCoinsCall{ + sendCall(addr1, addr4, scopeID1), + sendCall(addr2, addr4, scopeID2), + }, }, } for _, tc := range tests { s.Run(tc.name, func() { - var scopeIDs []types.MetadataAddress - handler := func(scopeID types.MetadataAddress) bool { - scopeIDs = append(scopeIDs, scopeID) - return tc.stopAfter > 0 && len(scopeIDs) >= tc.stopAfter + if tc.bankK == nil { + tc.bankK = NewMockBankKeeper() } + defer s.SwapBankKeeper(tc.bankK)() + + expBKCalls := BankKeeperCalls{ + SendCoins: tc.expSendCalls, + } + if tc.expBlockedCall { + addr := s.AccAddressFromBech32(tc.newVO, "new value owner") + expBKCalls.BlockedAddr = append(expBKCalls.BlockedAddr, addr) + } + var err error testFunc := func() { - err = s.app.MetadataKeeper.IterateScopesForValueOwner(s.FreshCtx(), tc.valueOwner, handler) + err = s.app.MetadataKeeper.SetScopeValueOwners(s.FreshCtx(), tc.links, tc.newVO) } - s.Require().NotPanics(testFunc, "IterateScopesForValueOwner") - s.AssertErrorValue(err, tc.expErr, "IterateScopesForValueOwner") - s.Assert().Equal(tc.expScopeIDs, scopeIDs, "scope ids iterated") + s.Require().NotPanics(testFunc, "SetScopeValueOwners") + s.AssertErrorValue(err, tc.expErr, "error from SetScopeValueOwners") + tc.bankK.AssertCalls(s.T(), expBKCalls) }) } } +func (s *ScopeKeeperTestSuite) TestMetadataScopeIterator() { + ctx := s.FreshCtx() + for i := 1; i <= 10; i++ { + valueOwner := "" + if i == 5 { + valueOwner = s.user2 + } + ns := types.NewScope(types.ScopeMetadataAddress(uuid.New()), nil, ownerPartyList(s.user1), []string{s.user1}, valueOwner, false) + s.app.MetadataKeeper.SetScope(ctx, *ns) + } + count := 0 + err := s.app.MetadataKeeper.IterateScopes(ctx, func(s types.Scope) (stop bool) { + count++ + return false + }) + s.Require().NoError(err, "IterateScopes") + s.Assert().Equal(10, count, "number of scopes iterated") + + count = 0 + err = s.app.MetadataKeeper.IterateScopesForAddress(ctx, s.user1Addr, func(scopeID types.MetadataAddress) (stop bool) { + count++ + s.True(scopeID.IsScopeAddress()) + return false + }) + s.Require().NoError(err, "IterateScopesForAddress user1") + s.Assert().Equal(10, count, "number of scope ids iterated for user1") + + count = 0 + err = s.app.MetadataKeeper.IterateScopesForAddress(ctx, s.user2Addr, func(scopeID types.MetadataAddress) (stop bool) { + count++ + s.True(scopeID.IsScopeAddress()) + return false + }) + s.Require().NoError(err, "IterateScopesForAddress user2") + s.Assert().Equal(0, count, "number of scope ids iterated for user2") + + count = 0 + err = s.app.MetadataKeeper.IterateScopes(ctx, func(s types.Scope) (stop bool) { + count++ + return count >= 5 + }) + s.Require().NoError(err, "IterateScopes with early stop") + s.Assert().Equal(5, count, "number of scopes iterated with early stop") +} + func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { ns := func(scopeID, scopeSpecification types.MetadataAddress, owners []types.Party, dataAccess []string, valueOwner string) *types.Scope { return &types.Scope{ @@ -514,13 +1294,13 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { s.app.MetadataKeeper.SetScopeSpecification(ctx, *scopeSpecSC) scopeID := types.ScopeMetadataAddress(uuid.New()) - scopeID2 := types.ScopeMetadataAddress(uuid.New()) // Give user 3 authority to sign for user 1 for scope updates. a := authz.NewGenericAuthorization(types.TypeURLMsgWriteScopeRequest) s.Require().NoError(s.app.AuthzKeeper.SaveGrant(ctx, s.user3Addr, s.user1Addr, a, nil), "authz SaveGrant user1 to user3") - otherAddr := sdk.AccAddress("other_address_______").String() + otherAddr := sdk.AccAddress("other_address_______") + otherAddrStr := otherAddr.String() cases := []struct { name string @@ -528,38 +1308,33 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { proposed types.Scope signers []string authzK *MockAuthzKeeper + bankK *MockBankKeeper errorMsg string + expAddrs []sdk.AccAddress }{ { name: "nil previous, proposed throws address error", existing: nil, proposed: types.Scope{}, signers: []string{s.user1}, - errorMsg: "address is empty", + errorMsg: "invalid scope metadata address MetadataAddress(nil): address is empty", }, { name: "valid proposed with nil existing doesn't error", existing: nil, proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), signers: []string{s.user1}, - errorMsg: "", - }, - { - name: "can't change scope id in update", - existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), - proposed: *ns(scopeID2, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), - signers: []string{s.user1}, - errorMsg: fmt.Sprintf("cannot update scope identifier. expected %s, got %s", scopeID.String(), scopeID2.String()), + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { - name: "missing existing owner signer on update fails", + name: "missing existing owner signer on update fails: adding data access", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{s.user1}, ""), signers: []string{s.user2}, errorMsg: fmt.Sprintf("missing signature: %s", s.user1), }, { - name: "missing existing owner signer on update fails", + name: "missing existing owner signer on update fails: changing owner", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user2), []string{}, ""), signers: []string{s.user2}, @@ -570,7 +1345,7 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{s.user1}, ""), signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { name: "no error when there are no updates regardless of signatures", @@ -578,18 +1353,19 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), signers: []string{}, errorMsg: "", + expAddrs: nil, }, { name: "setting value owner when unset does not error", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { name: "setting value owner when unset requires current owner signature", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), - proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), + proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user2), signers: []string{}, errorMsg: fmt.Sprintf("missing signature: %s", s.user1), }, @@ -598,105 +1374,105 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, ""), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user2), signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { name: "setting value owner to new user does not require their signature", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user2), signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { name: "no change to value owner should not error", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), signers: []string{s.user1}, - errorMsg: "", + expAddrs: nil, }, { - name: "setting a new value owner should not error with withdraw permission", + name: "changing value owner from marker succeeds", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, markerAddr), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { - name: "with rollup setting a new value owner should not error with withdraw permission", + name: "with rollup changing value owner from marker succeeds", existing: rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user1), markerAddr), proposed: *rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user1), s.user1), signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { - name: "setting a new value owner fails if missing withdraw permission", - existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user2), []string{}, markerAddr), - proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user2), []string{}, s.user2), + name: "setting a new value owner to a marker succeeds", + existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user2), []string{}, ""), + proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user2), []string{}, markerAddr), signers: []string{s.user2}, - errorMsg: fmt.Sprintf("missing signature for %s (testcoin) with authority to withdraw/remove it as scope value owner", markerAddr), + expAddrs: []sdk.AccAddress{s.user2Addr}, }, { - name: "with rollup setting a new value owner fails if missing withdraw permission", - existing: rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user2), markerAddr), - proposed: *rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user2), s.user2), + name: "with rollup setting a new value owner to a marker succeeds", + existing: rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user2), ""), + proposed: *rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user2), markerAddr), signers: []string{s.user2}, - errorMsg: fmt.Sprintf("missing signature for %s (testcoin) with authority to withdraw/remove it as scope value owner", markerAddr), + expAddrs: []sdk.AccAddress{s.user2Addr}, }, { - name: "setting a new value owner fails if missing deposit permission", - existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user2), []string{}, ""), + name: "changing value owner to a marker succeeds when existing is a signer", + existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user2), []string{}, s.user2), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user2), []string{}, markerAddr), signers: []string{s.user2}, - errorMsg: fmt.Sprintf("missing signature for %s (testcoin) with authority to deposit/add it as scope value owner", markerAddr), + expAddrs: []sdk.AccAddress{s.user2Addr}, }, { - name: "with rollup setting a new value owner fails if missing deposit permission", - existing: rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user2), ""), + name: "with rollup changing value owner to a marker succeeds when existing is a signer", + existing: rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user2), s.user2), proposed: *rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user2), markerAddr), signers: []string{s.user2}, - errorMsg: fmt.Sprintf("missing signature for %s (testcoin) with authority to deposit/add it as scope value owner", markerAddr), + expAddrs: []sdk.AccAddress{s.user2Addr}, }, { name: "setting a new value owner fails for scope owner when value owner signature is missing", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user2), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), signers: []string{s.user1}, - errorMsg: fmt.Sprintf("missing signature from existing value owner %s", s.user2), + errorMsg: "missing signature from existing value owner \"" + s.user2 + "\"", }, { name: "with rollup setting a new value owner fails for scope owner when value owner signature is missing", existing: rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user1), s.user2), proposed: *rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user1), s.user1), signers: []string{s.user1}, - errorMsg: fmt.Sprintf("missing signature from existing value owner %s", s.user2), + errorMsg: "missing signature from existing value owner \"" + s.user2 + "\"", }, { name: "changing only value owner only requires value owner sig", - existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2), []string{}, otherAddr), + existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2), []string{}, otherAddrStr), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2), []string{}, s.user1), - signers: []string{otherAddr}, - errorMsg: "", + signers: []string{otherAddrStr}, + expAddrs: []sdk.AccAddress{otherAddr}, }, { name: "with rollup changing only value owner only requires value owner sig", - existing: rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2), otherAddr), + existing: rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2), otherAddrStr), proposed: *rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2), s.user1), - signers: []string{otherAddr}, - errorMsg: "", + signers: []string{otherAddrStr}, + expAddrs: []sdk.AccAddress{otherAddr}, }, { name: "unsetting all fields on a scope should be successful", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), proposed: types.Scope{ScopeId: scopeID, SpecificationId: scopeSpecID, Owners: ownerPartyList(s.user1)}, signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { name: "setting specification id to nil should fail", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), proposed: *ns(scopeID, nil, ownerPartyList(s.user1), []string{}, s.user1), signers: []string{s.user1}, - errorMsg: "invalid specification id: address is empty", + errorMsg: "invalid scope specification metadata address MetadataAddress(nil): address is empty", }, { name: "setting unknown specification id should fail", @@ -710,42 +1486,40 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{s.user2}, s.user1), signers: []string{s.user3}, // user 1 has granted scope-write to user 3 - errorMsg: "", }, { name: "multi owner adding data access with authz grant should be successful", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2), []string{}, s.user1), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2), []string{s.user2}, s.user1), signers: []string{s.user2, s.user3}, // user 1 has granted scope-write to user 3 - errorMsg: "", }, { name: "changing value owner with authz grant should be successful", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user2), signers: []string{s.user3}, // user 1 has granted scope-write to user 3 - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user3Addr}, }, { name: "changing value owner by authz granter should be successful", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user2), signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { name: "changing value owner by non-authz grantee should fail", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user2), signers: []string{s.user2}, - errorMsg: fmt.Sprintf("missing signature from existing value owner %s", s.user1), + errorMsg: "missing signature from existing value owner \"" + s.user1 + "\"", }, { name: "changing value owner from non-authz granter with different signer should fail", existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user2), proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{}, s.user1), signers: []string{s.user3}, - errorMsg: fmt.Sprintf("missing signature from existing value owner %s", s.user2), + errorMsg: "missing signature from existing value owner \"" + s.user2 + "\"", }, { name: "setting value owner from nothing to non-owner only signed by non-owner should fail", @@ -765,8 +1539,8 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { name: "with rollup without existing but has req role and signer not involved in scope", existing: nil, proposed: *rollupScope(scopeID, scopeSpecID, ownerPartyList(s.user1), ""), - signers: []string{otherAddr}, - errorMsg: "", + signers: []string{otherAddrStr}, + expAddrs: []sdk.AccAddress{otherAddr}, }, { name: "with rollup existing required owner is not signer", @@ -787,7 +1561,7 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { existing: rollupScope(scopeID, scopeSpecID, ptz(pt(s.user1, owner, true), pt(s.user2, owner, true)), ""), proposed: *rollupScope(scopeID, scopeSpecID, ptz(pt(s.user2, owner, true)), ""), signers: []string{s.user2}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user2Addr}, }, { name: "smart contract account is not PROVENANCE role", @@ -852,7 +1626,7 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { ValueOwnerAddress: s.scUser, }, signers: []string{s.scUser, s.user1}, - errorMsg: "smart contract signer " + s.scUser + " is not authorized", + errorMsg: "missing signature from existing value owner \"" + s.user1 + "\"", }, { name: "with rollup only change is value owner signed by smart contract", @@ -872,10 +1646,10 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { RequirePartyRollup: true, }, signers: []string{s.scUser, s.user1}, - errorMsg: "smart contract signer " + s.scUser + " is not authorized", + errorMsg: "missing signature from existing value owner \"" + s.user1 + "\"", }, { - name: "only change is value owner signed by smart contract and authorized", + name: "only change is value owner signed by smart contract: with authz", existing: &types.Scope{ ScopeId: scopeID, SpecificationId: scopeSpecSC.SpecificationId, @@ -901,10 +1675,10 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { }, }, ), - errorMsg: "", + expAddrs: []sdk.AccAddress{s.scUserAddr}, }, { - name: "with rollup only change is value owner signed by smart contract", + name: "with rollup only change is value owner signed by smart contract: with authz", existing: &types.Scope{ ScopeId: scopeID, SpecificationId: scopeSpecSC.SpecificationId, @@ -932,7 +1706,7 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { }, }, ), - errorMsg: "", + expAddrs: []sdk.AccAddress{s.scUserAddr}, }, { name: "only change is smart contract value owner signed by smart contract", @@ -951,7 +1725,7 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { RequirePartyRollup: true, }, signers: []string{s.scUser}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.scUserAddr}, }, { name: "with rollup only change is smart contract value owner signed by smart contract", @@ -968,7 +1742,7 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { ValueOwnerAddress: s.user1, }, signers: []string{s.scUser}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.scUserAddr}, }, { name: "only change is value owner roles not checked with spec", @@ -987,7 +1761,7 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { ValueOwnerAddress: s.user2, }, signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, }, { name: "only change is value owner provenance roles not checked", @@ -1006,26 +1780,114 @@ func (s *ScopeKeeperTestSuite) TestValidateWriteScope() { ValueOwnerAddress: s.user2, }, signers: []string{s.user1}, - errorMsg: "", + expAddrs: []sdk.AccAddress{s.user1Addr}, + }, + { + name: "multiple signers with a value owner change to a marker", + existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2, s.user3), nil, s.user1), + proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2, s.user3), nil, markerAddr), + // Only the value owner is changing, so only the existing one needs to sign. But maybe user2 + // is the one with deposit, so it should be included in the returned addresses. + signers: []string{s.user1, s.user2}, + authzK: NewMockAuthzKeeper(), // So that there's no authz grants involved. + expAddrs: []sdk.AccAddress{s.user1Addr, s.user2Addr}, + }, + { + name: "multiple signers with just a value owner change from a marker", + existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2, s.user3), nil, markerAddr), + proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2, s.user3), nil, s.user1), + // Only the value owner is changing, so only the existing one needs to sign. However, since the + // existing is a marker, it can't sign. Validation should pass and all the signers should be + // returned as transfer agents so their marker permissions can be used during the SendCoins. + signers: []string{s.user1, s.user2}, + authzK: NewMockAuthzKeeper(), // So that there's no authz grants involved. + expAddrs: []sdk.AccAddress{s.user1Addr, s.user2Addr}, + }, + { + name: "multiple signers with just a value owner change, first signer and value owner is smart contract", + existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2, s.user3), nil, s.scUser), + proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1, s.user2, s.user3), nil, s.user1), + signers: []string{s.scUser, s.user1}, + expAddrs: []sdk.AccAddress{s.scUserAddr}, + }, + { + name: "updating, no proposed value owner: getting current value owner would give error", + existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), nil, s.user3), + proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{s.user2}, ""), + signers: []string{s.user1}, + authzK: NewMockAuthzKeeper(), // So that there's no authz grants involved. + bankK: NewMockBankKeeper().WithDenomOwnerError(scopeID, "this error should not be triggered"), + expAddrs: []sdk.AccAddress{s.user1Addr}, + }, + { + name: "updating, with proposed value owner: error getting current value owner", + existing: ns(scopeID, scopeSpecID, ownerPartyList(s.user1), nil, s.user3), + proposed: *ns(scopeID, scopeSpecID, ownerPartyList(s.user1), []string{s.user2}, s.user3), + signers: []string{s.user1}, + authzK: NewMockAuthzKeeper(), // So that there's no authz grants involved. + bankK: NewMockBankKeeper().WithDenomOwnerError(scopeID, "this is an injected error"), + errorMsg: "error identifying current value owner of \"" + scopeID.String() + "\": this is an injected error", + }, + { + name: "changing value owner and data access, scope has provenance role, two signers: smart contract, existing", + existing: &types.Scope{ + ScopeId: scopeID, + SpecificationId: scopeSpecSC.SpecificationId, + Owners: ptz(pt(s.scUser, types.PartyType_PARTY_TYPE_PROVENANCE, false)), + DataAccess: nil, + ValueOwnerAddress: s.user1, + }, + proposed: types.Scope{ + ScopeId: scopeID, + SpecificationId: scopeSpecSC.SpecificationId, + Owners: ptz(pt(s.scUser, types.PartyType_PARTY_TYPE_PROVENANCE, false)), + DataAccess: []string{s.user1}, + ValueOwnerAddress: s.user2, + }, + signers: []string{s.scUser, s.user1}, + // The second signer should be ignored for value owner signer checking because first is a smart contract. + errorMsg: "missing signature from existing value owner \"" + s.user1 + "\"", }, } for _, tc := range cases { s.Run(tc.name, func() { if tc.authzK != nil { - origAuthzK := s.app.MetadataKeeper.SetAuthzKeeper(tc.authzK) - defer s.app.MetadataKeeper.SetAuthzKeeper(origAuthzK) + defer s.SwapAuthzKeeper(tc.authzK)() } + if tc.bankK == nil { + tc.bankK = NewMockBankKeeper() + } + if tc.existing != nil && len(tc.existing.ValueOwnerAddress) > 0 { + // If there's supposed to be an existing value owner, and it hasn't been mocked yet, + // mock it now so that it can be properly looked up from the mock bank keeper later. + if _, has := tc.bankK.DenomOwnerResults[tc.existing.ScopeId.Denom()]; !has { + addr := s.AccAddressFromBech32(tc.existing.ValueOwnerAddress, "existing value owner") + tc.bankK = tc.bankK.WithDenomOwnerResult(tc.existing.ScopeId, addr) + } + } + defer s.SwapBankKeeper(tc.bankK)() + + // Use a cache context so the cases don't interact. + ctx, _ = s.FreshCtx().CacheContext() + if tc.existing != nil { + testWriteScope := func() { + s.app.MetadataKeeper.WriteScopeToState(ctx, *tc.existing) + } + s.Require().NotPanics(testWriteScope, "writeScopeToState") + } + msg := &types.MsgWriteScopeRequest{ Scope: tc.proposed, Signers: tc.signers, } - err = s.app.MetadataKeeper.ValidateWriteScope(s.FreshCtx(), tc.existing, msg) - if len(tc.errorMsg) > 0 { - s.Assert().EqualError(err, tc.errorMsg, "ValidateWriteScope expected error") - } else { - s.Assert().NoError(err, "ValidateWriteScope unexpected error") + var addrs []sdk.AccAddress + testFunc := func() { + addrs, err = s.app.MetadataKeeper.ValidateWriteScope(ctx, msg) } + s.Require().NotPanics(testFunc, "ValidateWriteScope") + s.AssertErrorValue(err, tc.errorMsg, "error from ValidateWriteScope") + s.Assert().Equal(tc.expAddrs, addrs, "addrs from ValidateWriteScope") }) } } @@ -1044,6 +1906,9 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { return rv } + owner := types.PartyType_PARTY_TYPE_OWNER + servicer := types.PartyType_PARTY_TYPE_SERVICER + ctx := s.FreshCtx() markerDenom := "testcoins2" markerAddr := markertypes.MustGetMarkerAddress(markerDenom).String() @@ -1072,7 +1937,7 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { DataAccess: nil, ValueOwnerAddress: "", } - s.app.MetadataKeeper.SetScope(ctx, scopeNoValueOwner) + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeNoValueOwner), "SetScope(scopeNoValueOwner)") scopeMarkerValueOwner := types.Scope{ ScopeId: types.ScopeMetadataAddress(uuid.New()), @@ -1081,7 +1946,7 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { DataAccess: nil, ValueOwnerAddress: markerAddr, } - s.app.MetadataKeeper.SetScope(ctx, scopeMarkerValueOwner) + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeMarkerValueOwner), "SetScope(scopeMarkerValueOwner)") scopeUserValueOwner := types.Scope{ ScopeId: types.ScopeMetadataAddress(uuid.New()), @@ -1090,10 +1955,31 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { DataAccess: nil, ValueOwnerAddress: s.user1, } - s.app.MetadataKeeper.SetScope(ctx, scopeUserValueOwner) + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeUserValueOwner), "SetScope(scopeUserValueOwner)") - owner := types.PartyType_PARTY_TYPE_OWNER - servicer := types.PartyType_PARTY_TYPE_SERVICER + scopeSCValueOwner := types.Scope{ + ScopeId: types.ScopeMetadataAddress(uuid.New()), + SpecificationId: types.ScopeSpecMetadataAddress(uuid.New()), + Owners: ptz( + pt(s.scUser, types.PartyType_PARTY_TYPE_PROVENANCE, true), + pt(s.user1, owner, false), + ), + ValueOwnerAddress: s.scUser, + RequirePartyRollup: true, + } + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeSCValueOwner), "SetScope(scopeSCValueOwner)") + + scopeUserValueOwnerWithSC := types.Scope{ + ScopeId: types.ScopeMetadataAddress(uuid.New()), + SpecificationId: types.ScopeSpecMetadataAddress(uuid.New()), + Owners: ptz( + pt(s.scUser, types.PartyType_PARTY_TYPE_PROVENANCE, true), + pt(s.user2, owner, false), + ), + ValueOwnerAddress: s.user1, + RequirePartyRollup: true, + } + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeUserValueOwnerWithSC), "SetScope(scopeUserValueOwnerWithSC)") scopeSpec := types.ScopeSpecification{ SpecificationId: types.ScopeSpecMetadataAddress(uuid.New()), @@ -1104,7 +1990,8 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { } s.app.MetadataKeeper.SetScopeSpecification(ctx, scopeSpec) - otherUser := sdk.AccAddress("some_other_user_____").String() + otherUserAddr := sdk.AccAddress("some_other_user_____") + otherUser := otherUserAddr.String() // with rollup no scope spec req party not signed scopeRollupNoSpecReq := types.Scope{ @@ -1115,7 +2002,7 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { ValueOwnerAddress: "", RequirePartyRollup: true, } - s.app.MetadataKeeper.SetScope(ctx, scopeRollupNoSpecReq) + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeRollupNoSpecReq), "SetScope(scopeRollupNoSpecReq)") // with rollup no scope spec all optional parties signer not involved scopeRollupNoSpecAllOpt := types.Scope{ @@ -1126,7 +2013,7 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { ValueOwnerAddress: "", RequirePartyRollup: true, } - s.app.MetadataKeeper.SetScope(ctx, scopeRollupNoSpecAllOpt) + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeRollupNoSpecAllOpt), "SetScope(scopeRollupNoSpecAllOpt)") // with rollup req scope owner not signed // with rollup req role not signed @@ -1139,7 +2026,7 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { ValueOwnerAddress: "", RequirePartyRollup: true, } - s.app.MetadataKeeper.SetScope(ctx, scopeRollup) + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeRollup), "SetScope(scopeRollup)") // with rollup marker value owner no signer has withdraw scopeRollupMarkerValueOwner := types.Scope{ @@ -1149,7 +2036,7 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { DataAccess: nil, ValueOwnerAddress: markerAddr, } - s.app.MetadataKeeper.SetScope(ctx, scopeRollupMarkerValueOwner) + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeRollupMarkerValueOwner), "SetScope(scopeRollupMarkerValueOwner)") // with rollup value owner not signed scopeRollupUserValueOwner := types.Scope{ @@ -1159,7 +2046,7 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { DataAccess: nil, ValueOwnerAddress: s.user1, } - s.app.MetadataKeeper.SetScope(ctx, scopeRollupUserValueOwner) + s.Require().NoError(s.app.MetadataKeeper.SetScope(ctx, scopeRollupUserValueOwner), "SetScope(scopeRollupUserValueOwner)") dneScopeID := types.ScopeMetadataAddress(uuid.New()) @@ -1173,192 +2060,200 @@ func (s *ScopeKeeperTestSuite) TestValidateDeleteScope() { tests := []struct { name string + bankK *MockBankKeeper scope types.Scope signers []string - expected string + expAddrs []sdk.AccAddress + expErr string }{ { name: "no value owner all signers", scope: scopeNoValueOwner, signers: []string{s.user1, s.user2}, - expected: "", + expAddrs: []sdk.AccAddress{s.user1Addr, s.user2Addr}, }, { name: "no value owner all signers reversed", scope: scopeNoValueOwner, - signers: []string{s.user1, s.user2}, - expected: "", + signers: []string{s.user2, s.user1}, + expAddrs: []sdk.AccAddress{s.user2Addr, s.user1Addr}, }, { name: "no value owner extra signer", scope: scopeNoValueOwner, signers: []string{s.user1, s.user2, s.user3}, - expected: "", + expAddrs: []sdk.AccAddress{s.user1Addr, s.user2Addr, s.user3Addr}, }, { - name: "no value owner missing signer 1", - scope: scopeNoValueOwner, - signers: []string{s.user2}, - expected: missing1Sig(s.user1), + name: "no value owner missing signer 1", + scope: scopeNoValueOwner, + signers: []string{s.user2}, + expErr: missing1Sig(s.user1), }, { - name: "no value owner missing signer 2", - scope: scopeNoValueOwner, - signers: []string{s.user1}, - expected: missing1Sig(s.user2), + name: "no value owner missing signer 2", + scope: scopeNoValueOwner, + signers: []string{s.user1}, + expErr: missing1Sig(s.user2), }, { - name: "no value owner no signers", - scope: scopeNoValueOwner, - signers: []string{}, - expected: missing2Sigs(s.user1, s.user2), + name: "no value owner no signers", + scope: scopeNoValueOwner, + signers: []string{}, + expErr: missing2Sigs(s.user1, s.user2), }, { - name: "no value owner wrong signer", - scope: scopeNoValueOwner, - signers: []string{s.user3}, - expected: missing2Sigs(s.user1, s.user2), + name: "no value owner wrong signer", + scope: scopeNoValueOwner, + signers: []string{s.user3}, + expErr: missing2Sigs(s.user1, s.user2), }, { name: "marker value owner signed by owner and user with auth", scope: scopeMarkerValueOwner, signers: []string{s.user1, s.user2}, - expected: "", + expAddrs: []sdk.AccAddress{s.user1Addr, s.user2Addr}, }, { name: "marker value owner signed by owner and user with auth reversed", scope: scopeMarkerValueOwner, signers: []string{s.user2, s.user1}, - expected: "", - }, - { - name: "marker value owner not signed by owner", - scope: scopeMarkerValueOwner, - signers: []string{s.user1}, - expected: missing1Sig(s.user2), + expAddrs: []sdk.AccAddress{s.user2Addr, s.user1Addr}, }, { - name: "marker value owner not signed by user with auth", - scope: scopeMarkerValueOwner, - signers: []string{s.user2}, - expected: fmt.Sprintf("missing signature for %s (testcoins2) with authority to withdraw/remove it as scope value owner", markerAddr), + name: "marker value owner not signed by owner", + scope: scopeMarkerValueOwner, + signers: []string{s.user1}, + expErr: missing1Sig(s.user2), }, { name: "user value owner signed by owner and value owner", scope: scopeUserValueOwner, signers: []string{s.user1, s.user2}, - expected: "", + expAddrs: []sdk.AccAddress{s.user1Addr, s.user2Addr}, }, { name: "user value owner signed by owner and value owner reversed", scope: scopeUserValueOwner, signers: []string{s.user2, s.user1}, - expected: "", + expAddrs: []sdk.AccAddress{s.user2Addr, s.user1Addr}, }, { - name: "user value owner not signed by owner", - scope: scopeUserValueOwner, - signers: []string{s.user1}, - expected: missing1Sig(s.user2), + name: "user value owner not signed by owner", + scope: scopeUserValueOwner, + signers: []string{s.user1}, + expErr: missing1Sig(s.user2), }, { - name: "user value owner not signed by value owner", - scope: scopeUserValueOwner, - signers: []string{s.user2}, - expected: fmt.Sprintf("missing signature from existing value owner %s", s.user1), + name: "user value owner not signed by value owner", + scope: scopeUserValueOwner, + signers: []string{s.user2}, + expErr: "missing signature from existing value owner \"" + s.user1 + "\"", }, { - name: "scope does not exist", - scope: types.Scope{ScopeId: dneScopeID}, - signers: []string{}, - expected: fmt.Sprintf("scope not found with id %s", dneScopeID), + name: "scope does not exist", + scope: types.Scope{ScopeId: dneScopeID}, + signers: []string{}, + expErr: "scope not found with id " + dneScopeID.String(), }, { - name: "with rollup no scope spec neither req party signed", - scope: scopeRollupNoSpecReq, - signers: []string{otherUser}, - expected: "missing signatures: " + s.user1 + ", " + s.user2 + "", + name: "with rollup no scope spec neither req party signed", + scope: scopeRollupNoSpecReq, + signers: []string{otherUser}, + expErr: missing2Sigs(s.user1, s.user2), }, { - name: "with rollup no scope spec req party 1 not signed", - scope: scopeRollupNoSpecReq, - signers: []string{s.user2}, - expected: "missing signature: " + s.user1, + name: "with rollup no scope spec req party 1 not signed", + scope: scopeRollupNoSpecReq, + signers: []string{s.user2}, + expErr: missing1Sig(s.user1), }, { - name: "with rollup no scope spec req party 2 not signed", - scope: scopeRollupNoSpecReq, - signers: []string{s.user1}, - expected: "missing signature: " + s.user2, + name: "with rollup no scope spec req party 2 not signed", + scope: scopeRollupNoSpecReq, + signers: []string{s.user1}, + expErr: missing1Sig(s.user2), }, { name: "with rollup no scope spec both req parties signed", scope: scopeRollupNoSpecReq, signers: []string{s.user1, s.user2}, - expected: "", + expAddrs: []sdk.AccAddress{s.user1Addr, s.user2Addr}, }, { name: "with rollup no scope spec all optional parties signer not involved", scope: scopeRollupNoSpecAllOpt, signers: []string{otherUser}, - expected: "", + expAddrs: []sdk.AccAddress{otherUserAddr}, }, { - name: "with rollup req scope owner not signed", - scope: scopeRollup, - signers: []string{s.user2, otherUser}, - expected: "missing required signature: " + s.user1 + " (OWNER)", + name: "with rollup req scope owner not signed", + scope: scopeRollup, + signers: []string{s.user2, otherUser}, + expErr: "missing required signature: " + s.user1 + " (OWNER)", }, { - name: "with rollup req role not signed", - scope: scopeRollup, - signers: []string{s.user1}, - expected: "missing signers for roles required by spec: SERVICER need 1 have 0", + name: "with rollup req role not signed", + scope: scopeRollup, + signers: []string{s.user1}, + expErr: "missing signers for roles required by spec: SERVICER need 1 have 0", }, { name: "with rollup req scope owner and req roles signed", scope: scopeRollup, signers: []string{s.user1, s.user2}, - expected: "", + expAddrs: []sdk.AccAddress{s.user1Addr, s.user2Addr}, }, { - name: "with rollup marker value owner no signer has withdraw", - scope: scopeRollupMarkerValueOwner, - signers: []string{s.user2}, - expected: "missing signature for " + markerAddr + " (testcoins2) with authority to withdraw/remove it as scope value owner", + name: "with rollup value owner not signed", + scope: scopeRollupUserValueOwner, + signers: []string{s.user2}, + expErr: "missing signature from existing value owner \"" + s.user1 + "\"", }, { - name: "with rollup marker value owner signer has withdraw", - scope: scopeRollupMarkerValueOwner, + name: "with rollup value owner signed", + scope: scopeRollupUserValueOwner, signers: []string{s.user1, s.user2}, - expected: "", + expAddrs: []sdk.AccAddress{s.user1Addr, s.user2Addr}, }, { - name: "with rollup value owner not signed", - scope: scopeRollupUserValueOwner, - signers: []string{s.user2}, - expected: "missing signature from existing value owner " + s.user1, + name: "error getting current value owner", + bankK: NewMockBankKeeper().WithDenomOwnerError(scopeUserValueOwner.ScopeId, "oopsies: no worky"), + scope: scopeUserValueOwner, + signers: []string{s.user1, s.user2}, + expErr: "error identifying current value owner of \"" + scopeUserValueOwner.ScopeId.String() + "\": oopsies: no worky", }, { - name: "with rollup value owner signed", - scope: scopeRollupUserValueOwner, - signers: []string{s.user1, s.user2}, - expected: "", + name: "first signer is smart contract and not value owner", + scope: scopeUserValueOwnerWithSC, + signers: []string{s.scUser, s.user1, s.user2}, + expErr: "missing signature from existing value owner \"" + s.user1 + "\"", + }, + { + name: "first signer is smart contract and value owner", + scope: scopeSCValueOwner, + signers: []string{s.scUser, s.user1}, + expAddrs: []sdk.AccAddress{s.scUserAddr}, // Just the first signers since it's sc. }, } for _, tc := range tests { - s.T().Run(tc.name, func(t *testing.T) { + s.Run(tc.name, func() { + if tc.bankK != nil { + defer s.SwapBankKeeper(tc.bankK)() + } + msg := &types.MsgDeleteScopeRequest{ ScopeId: tc.scope.ScopeId, Signers: tc.signers, } - actual := s.app.MetadataKeeper.ValidateDeleteScope(s.FreshCtx(), msg) - if len(tc.expected) > 0 { - require.EqualError(t, actual, tc.expected) - } else { - require.NoError(t, actual) + var addrs []sdk.AccAddress + testFunc := func() { + addrs, err = s.app.MetadataKeeper.ValidateDeleteScope(s.FreshCtx(), msg) } + s.Require().NotPanics(testFunc, "ValidateDeleteScope") + s.AssertErrorValue(err, tc.expErr, "error from ValidateDeleteScope") + s.Assert().Equal(tc.expAddrs, addrs, "addresses from ValidateDeleteScope") }) } } @@ -1871,7 +2766,7 @@ func (s *ScopeKeeperTestSuite) TestValidateScopeDeleteDataAccess() { } } -func (s *ScopeKeeperTestSuite) TestValidateScopeUpdateOwners() { +func (s *ScopeKeeperTestSuite) TestValidateUpdateScopeOwners() { pt := func(addr string, role types.PartyType, opt bool) types.Party { return types.Party{Address: addr, Role: role, Optional: opt} } @@ -2149,37 +3044,32 @@ func (s *ScopeKeeperTestSuite) TestScopeIndexing() { ctx := s.FreshCtx() store := ctx.KVStore(s.app.GetKey(types.ModuleName)) - s.T().Run("1 write new scope", func(t *testing.T) { + s.Run("1 write new scope", func() { expectedIndexes := []struct { key []byte name string }{ {types.GetAddressScopeCacheKey(ownerConstant.Addr, scopeID), "ownerConstant address index"}, {types.GetAddressScopeCacheKey(ownerToRemove.Addr, scopeID), "ownerToRemove address index"}, - {types.GetAddressScopeCacheKey(valueOwnerOrig.Addr, scopeID), "valueOwnerOrig address index"}, - - {types.GetValueOwnerScopeCacheKey(valueOwnerOrig.Addr, scopeID), "valueOwnerOrig value owner index"}, {types.GetScopeSpecScopeCacheKey(specIDOrig, scopeID), "specIDOrig spec index"}, } - s.app.MetadataKeeper.SetScope(ctx, scopeV1) + err := s.app.MetadataKeeper.SetScope(ctx, scopeV1) + s.Require().NoError(err, "SetScope") for _, expected := range expectedIndexes { - assert.True(t, store.Has(expected.key), expected.name) + s.Assert().True(store.Has(expected.key), expected.name) } }) - s.T().Run("2 update scope", func(t *testing.T) { + s.Run("2 update scope", func() { expectedIndexes := []struct { key []byte name string }{ {types.GetAddressScopeCacheKey(ownerConstant.Addr, scopeID), "ownerConstant address index"}, {types.GetAddressScopeCacheKey(ownerToAdd.Addr, scopeID), "ownerToAdd address index"}, - {types.GetAddressScopeCacheKey(valueOwnerNew.Addr, scopeID), "valueOwnerNew address index"}, - - {types.GetValueOwnerScopeCacheKey(valueOwnerNew.Addr, scopeID), "valueOwnerNew value owner index"}, {types.GetScopeSpecScopeCacheKey(specIDNew, scopeID), "specIDNew spec index"}, } @@ -2190,22 +3080,21 @@ func (s *ScopeKeeperTestSuite) TestScopeIndexing() { {types.GetAddressScopeCacheKey(ownerToRemove.Addr, scopeID), "ownerToRemove address index"}, {types.GetAddressScopeCacheKey(valueOwnerOrig.Addr, scopeID), "valueOwnerOrig address index"}, - {types.GetValueOwnerScopeCacheKey(valueOwnerOrig.Addr, scopeID), "valueOwnerOrig value owner index"}, - {types.GetScopeSpecScopeCacheKey(specIDOrig, scopeID), "specIDOrig spec index"}, } - s.app.MetadataKeeper.SetScope(ctx, scopeV2) + err := s.app.MetadataKeeper.SetScope(ctx, scopeV2) + s.Require().NoError(err, "SetScope") for _, expected := range expectedIndexes { - assert.True(t, store.Has(expected.key), expected.name) + s.Assert().True(store.Has(expected.key), expected.name) } for _, unexpected := range unexpectedIndexes { - assert.False(t, store.Has(unexpected.key), unexpected.name) + s.Assert().False(store.Has(unexpected.key), unexpected.name) } }) - s.T().Run("3 delete scope", func(t *testing.T) { + s.Run("3 delete scope", func() { unexpectedIndexes := []struct { key []byte name string @@ -2216,230 +3105,293 @@ func (s *ScopeKeeperTestSuite) TestScopeIndexing() { {types.GetAddressScopeCacheKey(valueOwnerOrig.Addr, scopeID), "valueOwnerOrig address index"}, {types.GetAddressScopeCacheKey(valueOwnerNew.Addr, scopeID), "valueOwnerNew address index"}, - {types.GetValueOwnerScopeCacheKey(valueOwnerOrig.Addr, scopeID), "valueOwnerOrig value owner index"}, - {types.GetValueOwnerScopeCacheKey(valueOwnerNew.Addr, scopeID), "valueOwnerNew value owner index"}, - {types.GetScopeSpecScopeCacheKey(specIDOrig, scopeID), "specIDOrig spec index"}, {types.GetScopeSpecScopeCacheKey(specIDNew, scopeID), "specIDNew spec index"}, } - s.app.MetadataKeeper.RemoveScope(ctx, scopeID) + err := s.app.MetadataKeeper.RemoveScope(ctx, scopeID) + s.Require().NoError(err, "RemoveScope") for _, unexpected := range unexpectedIndexes { - assert.False(t, store.Has(unexpected.key), unexpected.name) + s.Assert().False(store.Has(unexpected.key), unexpected.name) } }) } func (s *ScopeKeeperTestSuite) TestValidateUpdateValueOwners() { - scopeID1 := types.ScopeMetadataAddress(uuid.New()) - scopeID2 := types.ScopeMetadataAddress(uuid.New()) - scopeID3 := types.ScopeMetadataAddress(uuid.New()) - scopeID4 := types.ScopeMetadataAddress(uuid.New()) - - addr1 := sdk.AccAddress("addr1_______________") - addr2 := sdk.AccAddress("addr2_______________") - addr3 := sdk.AccAddress("addr3_______________") - addr4 := sdk.AccAddress("addr4_______________") - addrWithDeposit := sdk.AccAddress("addrWithDeposit_____") - addrSmartContract := sdk.AccAddress("addrSmartContract___") - - addr1Str := addr1.String() - addr2Str := addr2.String() - addr3Str := addr3.String() - addr4Str := addr4.String() - addrWithDepositStr := addrWithDeposit.String() - addrSmartContractStr := addrSmartContract.String() - - scope := func(scopeID types.MetadataAddress, valueOwner string) *types.Scope { - return &types.Scope{ - ScopeId: scopeID, - ValueOwnerAddress: valueOwner, - } + newUUID := func(i string) uuid.UUID { + str := strings.ReplaceAll("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "x", i) + rv, err := uuid.Parse(str) + s.Require().NoError(err, "uuid.Parse(%q)", str) + return rv } - - msg := func(signers ...string) *types.MsgUpdateValueOwnersRequest { - return &types.MsgUpdateValueOwnersRequest{ - Signers: signers, + scopeID1 := types.ScopeMetadataAddress(newUUID("1")) // scope1qqg3zyg3zyg3zyg3zyg3zyg3zygsd65l7v + scopeID2 := types.ScopeMetadataAddress(newUUID("2")) // scope1qq3zyg3zyg3zyg3zyg3zyg3zyg3qnhc8wg + scopeID3 := types.ScopeMetadataAddress(newUUID("3")) // scope1qqenxvenxvenxvenxvenxvenxvesqa360g + scopeID4 := types.ScopeMetadataAddress(newUUID("4")) // scope1qpzyg3zyg3zyg3zyg3zyg3zyg3zqyvqcrf + testlog.WriteVariables(s.T(), "scopes", + "scopeID1", scopeID1, + "scopeID2", scopeID2, + "scopeID3", scopeID3, + "scopeID4", scopeID4, + ) + + addr1 := sdk.AccAddress("1addr_______________") // cosmos1x9skgerjta047h6lta047h6lta047h6l4429yc + addr2 := sdk.AccAddress("2addr_______________") // cosmos1xfskgerjta047h6lta047h6lta047h6lh0rr9a + addr3 := sdk.AccAddress("3addr_______________") // cosmos1xdskgerjta047h6lta047h6lta047h6lw7ypa7 + addr4 := sdk.AccAddress("4addr_______________") // cosmos1x3skgerjta047h6lta047h6lta047h6lnj308h + addr5 := sdk.AccAddress("5addr_______________") // cosmos1x4skgerjta047h6lta047h6lta047h6l2rkdl5 + testlog.WriteVariables(s.T(), "addresses", + "addr1", addr1, + "addr2", addr2, + "addr3", addr3, + "addr4", addr4, + "addr5", addr5, + ) + + accStrs := func(addrs []sdk.AccAddress) []string { + if addrs == nil { + return nil + } + rv := make([]string, len(addrs)) + for i, addr := range addrs { + rv[i] = addr.String() } + return rv } - msgType := types.TypeURLMsgUpdateValueOwnersRequest - - markerDenom := "some.marker" - markerAddr := markertypes.MustGetMarkerAddress(markerDenom) - markerAddrStr := markerAddr.String() - marker := &markertypes.MarkerAccount{ - BaseAccount: authtypes.NewBaseAccount(markerAddr, nil, 0, 0), - Denom: markerDenom, - AccessControl: []markertypes.AccessGrant{ - { - Address: addrWithDepositStr, - Permissions: markertypes.AccessList{markertypes.Access_Deposit}, - }, + type msgMaker struct { + name string + msgType string + make func(signers []sdk.AccAddress) types.MetadataMsg + } + msgMakerUpdate := msgMaker{ + name: "update", + msgType: types.TypeURLMsgUpdateValueOwnersRequest, + make: func(signers []sdk.AccAddress) types.MetadataMsg { + return &types.MsgUpdateValueOwnersRequest{Signers: accStrs(signers)} + }, + } + msgMakerMigrate := msgMaker{ + name: "migrate", + msgType: types.TypeURLMsgMigrateValueOwnerRequest, + make: func(signers []sdk.AccAddress) types.MetadataMsg { + return &types.MsgMigrateValueOwnerRequest{Signers: accStrs(signers)} }, } + allMsgMakers := []msgMaker{msgMakerUpdate, msgMakerMigrate} - missingSig := func(addr string) string { - return "missing signature from existing value owner " + addr + missingSig := func(addr sdk.AccAddress) string { + return "missing signature from existing value owner \"" + addr.String() + "\"" } tests := []struct { - name string - scopes []*types.Scope - newValueOwner string - msg types.MetadataMsg - authK *MockAuthKeeper - authzK *MockAuthzKeeper - expErr string - expGetAccs []*GetAccountCall + name string + wasmAddrs []sdk.AccAddress + links types.AccMDLinks + proposed string + signers []sdk.AccAddress + expErr string + expAddrs []sdk.AccAddress }{ { - name: "one of the scopes does not have an existing value owner", - scopes: []*types.Scope{ - scope(scopeID1, addr1Str), scope(scopeID2, addr1Str), - scope(scopeID3, ""), scope(scopeID4, addr1Str), - }, - newValueOwner: addr1Str, - msg: msg(addr1Str), - expErr: "scope " + scopeID3.String() + " does not yet have a value owner", - expGetAccs: nil, + name: "nil links", + links: nil, + expErr: "no scopes found", }, { - name: "no signer for proposed", - scopes: []*types.Scope{scope(scopeID1, addr1Str)}, - newValueOwner: markerAddrStr, - msg: msg(), - authK: NewMockAuthKeeper().WithGetAccountResults(NewGetAccountCall(markerAddr, marker)), - expErr: fmt.Sprintf("missing signature for %s (%s) with authority to deposit/add it as scope value owner", markerAddrStr, markerDenom), - expGetAccs: []*GetAccountCall{ - NewGetAccountCall(markerAddr, marker), // checking if proposed is a marker - }, + name: "empty links", + links: types.AccMDLinks{}, + expErr: "no scopes found", }, { - name: "no signer for existing 1 of 3", - scopes: []*types.Scope{ - scope(scopeID1, addr1Str), scope(scopeID2, addr2Str), scope(scopeID3, addr3Str), - }, - newValueOwner: "", - msg: msg(addr2Str, addr3Str), - expErr: missingSig(addr1Str), - expGetAccs: []*GetAccountCall{ - NewGetAccountCall(addr1, nil), // checking if existing is a marker - NewGetAccountCall(addr2, nil), // checking if signer is wasm - NewGetAccountCall(addr3, nil), // checking if signer is wasm - }, + name: "nil entry in links", + links: types.AccMDLinks{{AccAddr: addr1, MDAddr: scopeID1}, nil, {AccAddr: addr2, MDAddr: scopeID2}}, + expErr: "nil entry not allowed", }, { - name: "no signer for existing 2 of 3", - scopes: []*types.Scope{ - scope(scopeID1, addr1Str), scope(scopeID2, addr2Str), scope(scopeID3, addr3Str), - }, - newValueOwner: "", - msg: msg(addr1Str, addr3Str), - expErr: missingSig(addr2Str), - expGetAccs: []*GetAccountCall{ - NewGetAccountCall(addr1, nil), // checking if existing is a marker - NewGetAccountCall(addr2, nil), // checking if existing is a marker - NewGetAccountCall(addr1, nil), // checking if signer is wasm - NewGetAccountCall(addr3, nil), // checking if signer is wasm - }, + name: "link without acc addr", + links: types.AccMDLinks{{AccAddr: nil, MDAddr: scopeID1}}, + expErr: "no account address associated with metadata address \"" + scopeID1.String() + "\"", }, { - name: "no signer for existing 3 of 3", - scopes: []*types.Scope{ - scope(scopeID1, addr1Str), scope(scopeID2, addr2Str), scope(scopeID3, addr3Str), - }, - newValueOwner: "", - msg: msg(addr1Str, addr2Str), - expErr: missingSig(addr3Str), - expGetAccs: []*GetAccountCall{ - NewGetAccountCall(addr1, nil), // checking if existing is a marker - NewGetAccountCall(addr2, nil), // checking if existing is a marker - NewGetAccountCall(addr3, nil), // checking if existing is a marker - NewGetAccountCall(addr1, nil), // checking if signer is wasm - NewGetAccountCall(addr2, nil), // checking if signer is wasm - }, - }, - { - name: "invalid smart contract signer", - scopes: []*types.Scope{ - scope(scopeID1, addr1Str), scope(scopeID2, addr2Str), scope(scopeID3, addr3Str), - }, - newValueOwner: addr4Str, - msg: msg(addr1Str, addr2Str, addr3Str, addrSmartContractStr), - authK: NewMockAuthKeeper().WithGetAccountResults(NewWasmGetAccountCall(addrSmartContract)), - expErr: "smart contract signer " + addrSmartContractStr + " cannot follow non-smart-contract signer", - expGetAccs: []*GetAccountCall{ - NewGetAccountCall(addr4, nil), // checking if proposed is a marker - NewGetAccountCall(addr1, nil), // checking if existing is a marker - NewGetAccountCall(addr2, nil), // checking if existing is a marker - NewGetAccountCall(addr3, nil), // checking if existing is a marker - NewGetAccountCall(addr1, nil), // checking if signer is wasm. - NewGetAccountCall(addr2, nil), // checking if signer is wasm. - NewGetAccountCall(addr3, nil), // checking if signer is wasm. - NewWasmGetAccountCall(addrSmartContract), // checking if signer is wasm. - }, - }, - { - name: "all scopes have same value owner authz used", - scopes: []*types.Scope{ - scope(scopeID1, addr1Str), scope(scopeID2, addr1Str), - scope(scopeID3, addr1Str), scope(scopeID4, addr1Str), - }, - newValueOwner: "", - msg: msg(addr2Str), - expErr: "", - authzK: NewMockAuthzKeeper().WithGetAuthorizationResults( - NewAcceptedGetAuthorizationCall(addr2, addr1, msgType, "one"), - ), - expGetAccs: []*GetAccountCall{ - NewGetAccountCall(addr1, nil), // checking if existing is a marker - // This one should happen only once for all scopes and other checks in there. - NewGetAccountCall(addr2, nil), // checking if signer is wasm - }, + name: "link without md addr", + links: types.AccMDLinks{{AccAddr: addr1, MDAddr: nil}}, + expErr: "invalid scope metadata address MetadataAddress(nil): address is empty", + }, + { + name: "duplicate md addr in links", + links: types.AccMDLinks{{AccAddr: addr1, MDAddr: scopeID1}, {AccAddr: addr1, MDAddr: scopeID1}}, + expErr: "duplicate metadata address \"" + scopeID1.String() + "\" not allowed", + }, + { + name: "one of the links already has the proposed acc address", + links: types.AccMDLinks{{AccAddr: addr1, MDAddr: scopeID1}, {AccAddr: addr2, MDAddr: scopeID2}}, + proposed: addr2.String(), + signers: []sdk.AccAddress{addr1, addr2}, + expErr: "scope \"" + scopeID2.String() + "\" " + + "already has the proposed value owner \"" + addr2.String() + "\"", }, { - name: "okay with a MsgMigrateValueOwnerRequest and authz", - scopes: []*types.Scope{scope(scopeID1, addr1Str)}, - newValueOwner: addr2Str, - msg: &types.MsgMigrateValueOwnerRequest{ - Existing: addr1Str, - Proposed: addr2Str, - Signers: []string{addr3Str}, + name: "two of the links already has the proposed acc address", + links: types.AccMDLinks{ + {AccAddr: addr1, MDAddr: scopeID1}, + {AccAddr: addr2, MDAddr: scopeID2}, + {AccAddr: addr1, MDAddr: scopeID3}, + {AccAddr: addr4, MDAddr: scopeID4}, }, - expErr: "", - authzK: NewMockAuthzKeeper().WithGetAuthorizationResults( - NewAcceptedGetAuthorizationCall(addr3, addr1, types.TypeURLMsgMigrateValueOwnerRequest, "one"), - ), - expGetAccs: []*GetAccountCall{ - NewGetAccountCall(addr2, nil), // checking if proposed is a marker - NewGetAccountCall(addr1, nil), // checking if existing is a marker - NewGetAccountCall(addr3, nil), // checking if signer is wasm + proposed: addr1.String(), + signers: []sdk.AccAddress{addr1, addr2, addr4}, + expErr: "scopes [\"" + scopeID1.String() + "\" \"" + scopeID3.String() + "\"] " + + "already have the proposed value owner \"" + addr1.String() + "\"", + }, + { + name: "first signer is wasm: only first signer returned", + wasmAddrs: []sdk.AccAddress{addr1}, + links: types.AccMDLinks{{AccAddr: addr1, MDAddr: scopeID1}, {AccAddr: addr1, MDAddr: scopeID2}}, + proposed: addr5.String(), + signers: []sdk.AccAddress{addr1, addr2}, + expAddrs: []sdk.AccAddress{addr1}, + }, + { + name: "first signer is wasm: missing sig from second", + wasmAddrs: []sdk.AccAddress{addr1}, + links: types.AccMDLinks{{AccAddr: addr1, MDAddr: scopeID1}, {AccAddr: addr2, MDAddr: scopeID2}}, + proposed: addr5.String(), + signers: []sdk.AccAddress{addr1, addr2}, + expErr: missingSig(addr2), + }, + { + name: "first signer is not wasm: all signers returned.", + wasmAddrs: []sdk.AccAddress{addr2}, // second one, just to show it doesn't matter. + links: types.AccMDLinks{{AccAddr: addr1, MDAddr: scopeID1}, {AccAddr: addr2, MDAddr: scopeID2}}, + proposed: addr5.String(), + signers: []sdk.AccAddress{addr1, addr2}, + expAddrs: []sdk.AccAddress{addr1, addr2}, + }, + { + name: "missing signature", + wasmAddrs: nil, + links: types.AccMDLinks{ + {AccAddr: addr1, MDAddr: scopeID1}, {AccAddr: addr2, MDAddr: scopeID2}, + {AccAddr: addr3, MDAddr: scopeID3}, {AccAddr: addr4, MDAddr: scopeID4}, }, + proposed: addr5.String(), + signers: []sdk.AccAddress{addr1, addr2, addr4}, + expErr: missingSig(addr3), + }, + } + + for _, tc := range tests { + for _, maker := range allMsgMakers { + s.Run(maker.name+": "+tc.name, func() { + // Ignore authz and marker stuff for these tests and assume that tests on ValidateScopeValueOwnersSigners hit that. + defer s.SwapAuthzKeeper(NewMockAuthzKeeper())() + defer s.SwapMarkerKeeper(NewMockMarkerKeeper())() + + msg := maker.make(tc.signers) + ctx := s.FreshCtx() + if len(tc.wasmAddrs) > 0 { + cache := types.GetAuthzCache(ctx) + for _, addr := range tc.wasmAddrs { + cache.SetIsWasm(addr, true) + } + } + + var addrs []sdk.AccAddress + var err error + testFunc := func() { + addrs, err = s.app.MetadataKeeper.ValidateUpdateValueOwners(ctx, tc.links, tc.proposed, msg) + } + s.Require().NotPanics(testFunc, "ValidateUpdateValueOwners") + s.AssertErrorValue(err, tc.expErr, "error from ValidateUpdateValueOwners") + s.Assert().Equal(tc.expAddrs, addrs, "addrs from ValidateUpdateValueOwners") + }) + } + } +} + +func (s *ScopeKeeperTestSuite) TestGetNetAssetValue() { + toUUID := func(base string) uuid.UUID { + rv, err := uuid.FromBytes([]byte(base)) + s.Require().NoError(err, "uuid.FromBytes([]byte(%q))", base) + return rv + } + scopeIDDNE := types.ScopeMetadataAddress(toUUID("does_not_exist__")) + scopeIDBad := types.ScopeMetadataAddress(toUUID("bad_bad_bad_bad_")) + priceDenomBad := "aproblem" + scopeIDOK := types.ScopeMetadataAddress(toUUID("okayokayokayokay")) + priceDenomOK := "aokay" + okNAV := types.NetAssetValue{ + Price: sdk.NewInt64Coin(priceDenomOK, 987_123_654), + Volume: 1, + } + + setupStore := func() { + ctx := s.FreshCtx() + err := s.app.MetadataKeeper.SetNetAssetValue(ctx, scopeIDOK, okNAV, "testing") + s.Require().NoError(err) + + store := ctx.KVStore(s.app.MetadataKeeper.GetStoreKey()) + badKey := types.NetAssetValueKey(scopeIDBad, priceDenomBad) + badVal := []byte{0, 0, 0} + store.Set(badKey, badVal) + } + setupStore() + + tests := []struct { + name string + mdDenom string + pDenom string + expNAV *types.NetAssetValue + expErr string + }{ + { + name: "not a metadata denom", + mdDenom: "nope", + pDenom: "whatever", + expErr: "could not get metadata address: denom \"nope\" is not a MetadataAddress denom", + }, + { + name: "scope does not exist", + mdDenom: scopeIDDNE.Denom(), + pDenom: "whatever", + expNAV: nil, + }, + { + name: "nav does not exist", + mdDenom: scopeIDOK.Denom(), + pDenom: "whatever", + expNAV: nil, + }, + { + name: "invalid nav data in state", + mdDenom: scopeIDBad.Denom(), + pDenom: priceDenomBad, + expErr: "could not read nav for \"" + scopeIDBad.String() + "\" with price denom \"" + priceDenomBad + "\": proto: NetAssetValue: illegal tag 0 (wire type 0)", + }, + { + name: "okay", + mdDenom: scopeIDOK.Denom(), + pDenom: priceDenomOK, + expNAV: &okNAV, }, } for _, tc := range tests { s.Run(tc.name, func() { - if tc.authK == nil { - tc.authK = NewMockAuthKeeper() + ctx := s.FreshCtx() + var actNAV *types.NetAssetValue + var err error + testFunc := func() { + actNAV, err = s.app.MetadataKeeper.GetNetAssetValue(ctx, tc.mdDenom, tc.pDenom) } - if tc.authzK == nil { - tc.authzK = NewMockAuthzKeeper() + s.Require().NotPanics(testFunc, "GetNetAssetValue") + s.AssertErrorValue(err, tc.expErr, "error returned from GetNetAssetValue") + if !s.Assert().Equal(tc.expNAV, actNAV, "NAV returned from GetNetAssetValue") && tc.expNAV != nil && actNAV != nil { + s.Assert().Equal(tc.expNAV.Price.String(), actNAV.Price.String(), "NAV.Price (string)") + s.Assert().Equal(fmt.Sprintf("%d", tc.expNAV.UpdatedBlockHeight), fmt.Sprintf("%d", actNAV.UpdatedBlockHeight), "UpdatedBlockHeight (string)") + s.Assert().Equal(int(tc.expNAV.Volume), int(actNAV.Volume), "NAV.Volume (int)") } - mdKeeper := s.app.MetadataKeeper - origAuthK := mdKeeper.SetAuthKeeper(tc.authK) - origAuthzK := mdKeeper.SetAuthzKeeper(tc.authzK) - defer func() { - mdKeeper.SetAuthKeeper(origAuthK) - mdKeeper.SetAuthzKeeper(origAuthzK) - }() - - err := mdKeeper.ValidateUpdateValueOwners(s.FreshCtx(), tc.scopes, tc.newValueOwner, tc.msg) - s.AssertErrorValue(err, tc.expErr, "ValidateUpdateValueOwners") - - getAccs := tc.authK.GetAccountCalls - s.Assert().Equal(tc.expGetAccs, getAccs, "calls made to GetAccount") }) } } diff --git a/x/metadata/keeper/session.go b/x/metadata/keeper/session.go index 2ba45c71f4..520154ed05 100644 --- a/x/metadata/keeper/session.go +++ b/x/metadata/keeper/session.go @@ -167,7 +167,7 @@ func (k Keeper) ValidateWriteSession(ctx sdk.Context, existing *types.Session, m if err = validateRolesPresent(proposed.Parties, contractSpec.PartiesInvolved); err != nil { return err } - if err = k.validateProvenanceRole(ctx, BuildPartyDetails(nil, proposed.Parties)); err != nil { + if err = k.validateProvenanceRole(ctx, types.BuildPartyDetails(nil, proposed.Parties)); err != nil { return err } if err = k.ValidateSignersWithoutParties(ctx, scope.GetAllOwnerAddresses(), msg); err != nil { @@ -193,7 +193,7 @@ func (k Keeper) ValidateWriteSession(ctx sdk.Context, existing *types.Session, m if err = validateRolesPresent(proposed.Parties, contractSpec.PartiesInvolved); err != nil { return err } - if err = k.validateProvenanceRole(ctx, BuildPartyDetails(nil, proposed.Parties)); err != nil { + if err = k.validateProvenanceRole(ctx, types.BuildPartyDetails(nil, proposed.Parties)); err != nil { return err } reqParties = append(reqParties, existing.Parties...) diff --git a/x/metadata/keeper/session_test.go b/x/metadata/keeper/session_test.go index c0f49e5f89..c41ec16647 100644 --- a/x/metadata/keeper/session_test.go +++ b/x/metadata/keeper/session_test.go @@ -14,7 +14,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simapp "github.com/provenance-io/provenance/app" - "github.com/provenance-io/provenance/x/metadata/keeper" "github.com/provenance-io/provenance/x/metadata/types" ) @@ -85,7 +84,7 @@ func (s *SessionKeeperTestSuite) SetupTest() { } func (s *SessionKeeperTestSuite) FreshCtx() sdk.Context { - return keeper.AddAuthzCacheToContext(s.app.BaseApp.NewContext(false)) + return FreshCtx(s.app) } func TestSessionKeeperTestSuite(t *testing.T) { diff --git a/x/metadata/keeper/signers.go b/x/metadata/keeper/signers.go index 4c40c04c99..6bb80f2f0b 100644 --- a/x/metadata/keeper/signers.go +++ b/x/metadata/keeper/signers.go @@ -8,7 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - markertypes "github.com/provenance-io/provenance/x/marker/types" + "github.com/provenance-io/provenance/internal/provutils" "github.com/provenance-io/provenance/x/metadata/types" ) @@ -50,7 +50,7 @@ func (k Keeper) ValidateSignersWithParties( if err = k.validateProvenanceRole(ctx, parties); err != nil { return err } - return k.validateSmartContractSigners(ctx, GetUsedSigners(parties), msg) + return k.validateSmartContractSigners(ctx, types.GetUsedSigners(parties), msg) } // validateAllRequiredPartiesSigned ensures the following: @@ -65,8 +65,8 @@ func (k Keeper) validateAllRequiredPartiesSigned( reqParties, availableParties []types.Party, reqRoles []types.PartyType, msg types.MetadataMsg, -) ([]*PartyDetails, error) { - parties := BuildPartyDetails(reqParties, availableParties) +) ([]*types.PartyDetails, error) { + parties := types.BuildPartyDetails(reqParties, availableParties) signers := NewSignersWrapper(msg.GetSignerStrs()) // Make sure all required parties are signers. @@ -79,7 +79,7 @@ func (k Keeper) validateAllRequiredPartiesSigned( for i, party := range missingReqParties { missing[i] = fmt.Sprintf("%s (%s)", party.GetAddress(), party.GetRole().SimpleString()) } - return nil, fmt.Errorf("missing required signature%s: %s", pluralEnding(len(missing)), strings.Join(missing, ", ")) + return nil, fmt.Errorf("missing required signature%s: %s", provutils.PluralEnding(missing), strings.Join(missing, ", ")) } // Make sure all required roles are present as signers. @@ -95,9 +95,9 @@ func (k Keeper) validateAllRequiredPartiesSigned( return parties, nil } -// associateSigners updates each PartyDetails to indicate there's a signer if its +// associateSigners updates each types.PartyDetails to indicate there's a signer if its // address is in the signers list. -func associateSigners(parties []*PartyDetails, signers *SignersWrapper) { +func associateSigners(parties []*types.PartyDetails, signers *SignersWrapper) { if signers == nil { return } @@ -114,8 +114,8 @@ func associateSigners(parties []*PartyDetails, signers *SignersWrapper) { // findUnsignedRequired returns a list of parties that are required (optional=false) // and don't have a signer. -func findUnsignedRequired(parties []*PartyDetails) []*PartyDetails { - var rv []*PartyDetails +func findUnsignedRequired(parties []*types.PartyDetails) []*types.PartyDetails { + var rv []*types.PartyDetails for _, party := range parties { if party.IsRequired() && !party.HasSigner() { rv = append(rv, party) @@ -129,7 +129,7 @@ func findUnsignedRequired(parties []*PartyDetails) []*PartyDetails { // // This is similar to validateRolesPresent except this requires a role to have a signer // in order for it to fulfill a required role. -func associateRequiredRoles(parties []*PartyDetails, reqRoles []types.PartyType) []types.PartyType { +func associateRequiredRoles(parties []*types.PartyDetails, reqRoles []types.PartyType) []types.PartyType { var missingRoles []types.PartyType reqRolesLoop: for _, role := range reqRoles { @@ -146,7 +146,7 @@ reqRolesLoop: // missingRolesString generates and returns an error message indicating that // some required roles don't have signers. -func missingRolesString(parties []*PartyDetails, reqRoles []types.PartyType) string { +func missingRolesString(parties []*types.PartyDetails, reqRoles []types.PartyType) string { // Get a count for each required role reqCountByRole := make(map[types.PartyType]int) for _, role := range reqRoles { @@ -157,7 +157,7 @@ func missingRolesString(parties []*PartyDetails, reqRoles []types.PartyType) str haveCountByRole := make(map[types.PartyType]int) for _, party := range parties { if party.IsUsed() { - haveCountByRole[party.role]++ + haveCountByRole[party.GetRole()]++ } } @@ -196,8 +196,7 @@ func getAuthzMessageTypeURLs(msgTypeURL string) []string { } switch msgTypeURL { case types.TypeURLMsgAddScopeDataAccessRequest, types.TypeURLMsgDeleteScopeDataAccessRequest, - types.TypeURLMsgAddScopeOwnerRequest, types.TypeURLMsgDeleteScopeOwnerRequest, - types.TypeURLMsgUpdateValueOwnersRequest, types.TypeURLMsgMigrateValueOwnerRequest: + types.TypeURLMsgAddScopeOwnerRequest, types.TypeURLMsgDeleteScopeOwnerRequest: urls = append(urls, types.TypeURLMsgWriteScopeRequest) case types.TypeURLMsgWriteRecordRequest: urls = append(urls, types.TypeURLMsgWriteSessionRequest) @@ -224,7 +223,7 @@ func (k Keeper) findAuthzGrantee( if len(granter) == 0 || len(grantees) == 0 { return nil, nil } - cache := GetAuthzCache(ctx) + cache := types.GetAuthzCache(ctx) msgTypes := getAuthzMessageTypeURLs(sdk.MsgTypeURL(msg)) for _, grantee := range grantees { for _, msgType := range msgTypes { @@ -264,10 +263,10 @@ func (k Keeper) findAuthzGrantee( // to stop checking (i.e. true => stop now, false => keep checking the rest of the parties). func (k Keeper) associateAuthorizations( ctx sdk.Context, - parties []*PartyDetails, + parties []*types.PartyDetails, signers *SignersWrapper, msg types.MetadataMsg, - onAssociation func(party *PartyDetails) (stop bool), + onAssociation func(party *types.PartyDetails) (stop bool), ) error { for _, party := range parties { if !party.HasSigner() { @@ -299,20 +298,20 @@ func (k Keeper) associateAuthorizations( func (k Keeper) associateAuthorizationsForRoles( ctx sdk.Context, roles []types.PartyType, - parties []*PartyDetails, + parties []*types.PartyDetails, signers *SignersWrapper, msg types.MetadataMsg, ) (bool, error) { missingRoles := false for _, role := range roles { found := false - var partiesToCheck []*PartyDetails + var partiesToCheck []*types.PartyDetails for _, party := range parties { if party.IsStillUsableAs(role) && !party.HasSigner() { partiesToCheck = append(partiesToCheck, party) } } - err := k.associateAuthorizations(ctx, partiesToCheck, signers, msg, func(party *PartyDetails) bool { + err := k.associateAuthorizations(ctx, partiesToCheck, signers, msg, func(party *types.PartyDetails) bool { party.MarkAsUsed() found = true return true @@ -332,7 +331,7 @@ func (k Keeper) associateAuthorizationsForRoles( // validateProvenanceRole makes sure that: // - All parties with the address of a smart contract have the PROVENANCE role. // - All parties with the PROVENANCE role have the address of a smart contract. -func (k Keeper) validateProvenanceRole(ctx sdk.Context, parties []*PartyDetails) error { +func (k Keeper) validateProvenanceRole(ctx sdk.Context, parties []*types.PartyDetails) error { for _, party := range parties { if party.CanBeUsed() { // Using the party address here (instead of the signer) because it's @@ -359,7 +358,7 @@ func (k Keeper) isWasmAccount(ctx sdk.Context, addr sdk.AccAddress) bool { if len(addr) == 0 { return false } - authzCache := GetAuthzCache(ctx) + authzCache := types.GetAuthzCache(ctx) if authzCache.HasIsWasm(addr) { return authzCache.GetIsWasm(addr) } @@ -373,7 +372,7 @@ func (k Keeper) isWasmAccount(ctx sdk.Context, addr sdk.AccAddress) bool { // are in the usedSigners map or are authorized by all signers after them. // The usedSigners map has bech32 keys and value indicating whether that address was // used as a signer in some capacity (e.g. they're a party). -func (k Keeper) validateSmartContractSigners(ctx sdk.Context, usedSigners UsedSignersMap, msg types.MetadataMsg) error { +func (k Keeper) validateSmartContractSigners(ctx sdk.Context, usedSigners types.UsedSignersMap, msg types.MetadataMsg) error { // When a smart contract is a signer, they must either be used as a signer // already, or must be authorized by all signers after it. // The wasm encoders (hopefully) put the smart contract as the first signer @@ -423,122 +422,98 @@ func (k Keeper) validateSmartContractSigners(ctx sdk.Context, usedSigners UsedSi return nil } -// ValidateScopeValueOwnerUpdate verifies that it's okay for the msg signers to -// change a scope's value owner from existing to proposed. -// If some parties have already been validated (possibly utilizing authz), they -// can be provided in order to prevent an authorization from being used twice during -// a single Tx. -// -// If no error is returned, a map of bech32 strings to true is returned where each key -// is a signer that either has a signer in validatedParties, or is used directly in here. -func (k Keeper) ValidateScopeValueOwnerUpdate( +// ValidateScopeValueOwnersSigners ensures that all of the existingOwners are signers of the provided msg for the purposes +// of updating a value owner. Returns the list of possible transfer agents and a map indicating which signers were used. +func (k Keeper) ValidateScopeValueOwnersSigners( ctx sdk.Context, - existing, + existingOwners []sdk.AccAddress, proposed string, msg types.MetadataMsg, -) (UsedSignersMap, error) { - if existing == proposed { - return NewUsedSignersMap(), nil - } - signers := NewSignersWrapper(msg.GetSignerStrs()) - - usedSigners, err := k.validateScopeValueOwnerChangeFromExisting(ctx, existing, signers, msg) - if err != nil { - return nil, err - } - - newUsedSigners, err := k.validateScopeValueOwnerChangeToProposed(ctx, proposed, signers) - if err != nil { - return nil, err +) ([]sdk.AccAddress, types.UsedSignersMap, error) { + // If there's only one existing owner and it equals the proposed, then there's no need + // for transfer agents (nothing needs to be sent); we can return early. + if len(existingOwners) == 1 && existingOwners[0].String() == proposed { + return nil, types.NewUsedSignersMap(), nil + } + + // If the first signer is a smart contract, ignore all other signers in the msg. + // Only the existing value owner is allowed to change the value owner (regardless of the scope's parties). + // If it's a smart contract doing this, it'll be the first signer provided, and we ignore all other signers. + // So the smart contract must either be the existing owner or else all existing owners must have an authz + // grant for it. It's probably not a good idea to authz grant a smart contract though, but it's allowed. + signerStrs := msg.GetSignerStrs() + var signerAccs []sdk.AccAddress + if len(signerStrs) > 0 { + signer0, err := sdk.AccAddressFromBech32(signerStrs[0]) + if err != nil { + return nil, nil, fmt.Errorf("invalid signer address %q: %w", signerStrs[0], err) + } + if k.isWasmAccount(ctx, signer0) { + signerAccs = make([]sdk.AccAddress, 1) + if len(signerStrs) > 1 { + signerStrs = signerStrs[:1] + } + } else { + signerAccs = make([]sdk.AccAddress, len(signerStrs)) + } + signerAccs[0] = signer0 + for i := 1; i < len(signerStrs); i++ { + signerStr := signerStrs[i] + signerAccs[i], err = sdk.AccAddressFromBech32(signerStr) + if err != nil { + return nil, nil, fmt.Errorf("invalid signer[%d] address %q: %w", i, signerStr, err) + } + } } - return usedSigners.AlsoUse(newUsedSigners), nil -} - -// validateScopeValueOwnerChangeFromExisting validates that the provided signers -// are allowed to change the existing value owner. -func (k Keeper) validateScopeValueOwnerChangeFromExisting( - ctx sdk.Context, - existing string, - signers *SignersWrapper, - msg types.MetadataMsg, -) (UsedSignersMap, error) { - usedSigners := NewUsedSignersMap() - - // Nothing to check (in here) if the existing is empty. - if len(existing) == 0 { - return usedSigners, nil - } + usedSigners := types.NewUsedSignersMap() + for _, existing := range existingOwners { + // If it's empty, there's nothing to check. + if len(existing) == 0 { + continue + } - // If the existing is a marker, make sure a signer has withdraw authority on it. - marker, hasAuth, accWithAccess := k.GetMarkerAndCheckAuthority(ctx, existing, signers.Strings(), markertypes.Access_Withdraw) - if marker != nil { - if !hasAuth { - return nil, fmt.Errorf("missing signature for %s (%s) with authority to withdraw/remove it as scope value owner", existing, marker.GetDenom()) + // If the required signer is the same as the proposed value, there's no change, so a signer isn't needed. + existingStr := existing.String() + if existingStr == proposed { + continue } - return usedSigners.Use(accWithAccess), nil - } - // If the existing isn't a marker, make sure they're one of the signers or - // have an authorization grant for one of the signers. - for _, signer := range signers.Strings() { - if existing == signer { - return usedSigners.Use(signer), nil + // If it's one of the usable signers, there's nothing more to check for this one. + if containsAddr(signerAccs, existing) { + usedSigners.Use(existingStr) + continue } - } - // Not a signer. Check with authz for help. - // If existing isn't a bech32, we just skip the authz check. Should only happen in unit tests. - granter, err := sdk.AccAddressFromBech32(existing) - if err == nil { - // For the value owner address, we only check authz for non smart-contract signers - // This prevents Alice from using a smart contract to update Bob's - // scope when both have authorized the smart contract to WriteScope. - // But it allows Bob to authorize Alice and then Alice can update Bob's scope regardless - // of whether it's by means of a smart contract. - var grantees []sdk.AccAddress - for _, signer := range signers.Accs() { - if !k.isWasmAccount(ctx, signer) { - grantees = append(grantees, signer) - } + // If it's a marker address, there's no way it signed, but we'll later provide the signers + // as transfer agents with SendCoins. That will allow the marker module to correctly + // check for deposit or withdraw among the signers and return an error then if appropriate. + if k.markerKeeper.IsMarkerAccount(ctx, existing) { + continue } - grantee, err := k.findAuthzGrantee(ctx, granter, grantees, msg) - if err != nil { - return nil, fmt.Errorf("authz error with existing value owner %q: %w", existing, err) + + // Not a direct signer, and not a marker. Check with authz for an applicable grant. + grantee, authzErr := k.findAuthzGrantee(ctx, existing, signerAccs, msg) + if authzErr != nil { + return nil, nil, fmt.Errorf("authz error with existing value owner %q: %w", existingStr, authzErr) } - if len(grantee) > 0 { - return usedSigners.Use(grantee.String()), nil + if len(grantee) == 0 { + return nil, nil, fmt.Errorf("missing signature from existing value owner %q", existingStr) } + usedSigners.Use(grantee.String()) } - return nil, fmt.Errorf("missing signature from existing value owner %s", existing) + return signerAccs, usedSigners, nil } -// validateScopeValueOwnerChangeToProposed validates that the provided signers -// are allowed to set the value owner to the proposed value. -func (k Keeper) validateScopeValueOwnerChangeToProposed( - ctx sdk.Context, - proposed string, - signers *SignersWrapper, -) (UsedSignersMap, error) { - usedSigners := NewUsedSignersMap() - - // Nothing to check if the proposed is empty. - if len(proposed) == 0 { - return usedSigners, nil - } - - // If the proposed is a marker, make sure a signer has deposit authority on it. - marker, hasAuth, accWithAccess := k.GetMarkerAndCheckAuthority(ctx, proposed, signers.Strings(), markertypes.Access_Deposit) - if marker != nil { - if !hasAuth { - return nil, fmt.Errorf("missing signature for %s (%s) with authority to deposit/add it as scope value owner", proposed, marker.GetDenom()) +// containsAddr returns true if the addr toFind is equal to one (or more) of the provided addrs, false otherwise. +func containsAddr(addrs []sdk.AccAddress, toFind sdk.AccAddress) bool { + for _, addr := range addrs { + if toFind.Equals(addr) { + return true } - return usedSigners.Use(accWithAccess), nil } - - // If the proposed isn't a marker, we don't really care what it's being set to and no one needs to sign. - return usedSigners, nil + return false } // ValidateSignersWithoutParties makes sure that each entry in the required list are either signers of the msg, @@ -555,21 +530,20 @@ func (k Keeper) ValidateSignersWithoutParties( if err != nil { return err } - return k.validateSmartContractSigners(ctx, GetUsedSigners(parties), msg) + return k.validateSmartContractSigners(ctx, types.GetUsedSigners(parties), msg) } // validateAllRequiredSigned ensures that all required addresses are either in the msg signers, // or have granted an authorization to someone in the signers. // // If you call this, you will probably also need to call validateSmartContractSigners on your own. -func (k Keeper) validateAllRequiredSigned(ctx sdk.Context, required []string, msg types.MetadataMsg) ([]*PartyDetails, error) { - details := make([]*PartyDetails, len(required)) +func (k Keeper) validateAllRequiredSigned(ctx sdk.Context, required []string, msg types.MetadataMsg) ([]*types.PartyDetails, error) { + details := make([]*types.PartyDetails, len(required)) for i, addr := range required { - details[i] = &PartyDetails{ - address: addr, - role: types.PartyType_PARTY_TYPE_UNSPECIFIED, - optional: false, - } + details[i] = types.WrapRequiredParty(types.Party{ + Address: addr, + Role: types.PartyType_PARTY_TYPE_UNSPECIFIED, + }) } signers := NewSignersWrapper(msg.GetSignerStrs()) @@ -591,7 +565,7 @@ func (k Keeper) validateAllRequiredSigned(ctx sdk.Context, required []string, ms for i, party := range missingReqParties { missing[i] = party.GetAddress() } - return nil, fmt.Errorf("missing signature%s: %s", pluralEnding(len(missing)), strings.Join(missing, ", ")) + return nil, fmt.Errorf("missing signature%s: %s", provutils.PluralEnding(missing), strings.Join(missing, ", ")) } return details, nil @@ -601,7 +575,7 @@ func (k Keeper) validateAllRequiredSigned(ctx sdk.Context, required []string, ms // // This is similar to associateRequiredRoles, except this one doesn't require the party to have a signer. func validateRolesPresent(parties []types.Party, reqRoles []types.PartyType) error { - details := BuildPartyDetails(nil, parties) + details := types.BuildPartyDetails(nil, parties) roleMissing := false reqRolesLoop: for _, role := range reqRoles { @@ -621,7 +595,7 @@ reqRolesLoop: // validatePartiesArePresent returns an error if there are any parties in required that are not in available. func validatePartiesArePresent(required, available []types.Party) error { - missing := findMissingParties(required, available) + missing := types.FindMissingParties(required, available) if len(missing) == 0 { return nil } @@ -629,45 +603,5 @@ func validatePartiesArePresent(required, available []types.Party) error { for i, party := range missing { parts[i] = fmt.Sprintf("%s (%s)", party.Address, party.Role.SimpleString()) } - word := "party" - if len(missing) != 1 { - word = "parties" - } - return fmt.Errorf("missing %s: %s", word, strings.Join(parts, ", ")) -} - -// GetMarkerAndCheckAuthority gets a marker by address and checks if one of the signers has the provided role. -// If the address isn't a marker, nil, false is returned. -// The signer that has the requested permission is also returned. -func (k Keeper) GetMarkerAndCheckAuthority( - ctx sdk.Context, - address string, - signers []string, - role markertypes.Access, -) (markertypes.MarkerAccountI, bool, string) { - addr, err := sdk.AccAddressFromBech32(address) - // if the address is invalid then it is not possible for it to be a marker. - if err != nil { - return nil, false, "" - } - - acc := k.authKeeper.GetAccount(ctx, addr) - if acc == nil { - return nil, false, "" - } - - // Convert over to the actual underlying marker type, or not. - marker, isMarker := acc.(*markertypes.MarkerAccount) - if !isMarker { - return nil, false, "" - } - - // Check if any of the signers have the desired role. - for _, signer := range signers { - if marker.HasAccess(signer, role) { - return marker, true, signer - } - } - - return marker, false, "" + return fmt.Errorf("missing %s: %s", provutils.Pluralize(missing, "party", "parties"), strings.Join(parts, ", ")) } diff --git a/x/metadata/keeper/signers_test.go b/x/metadata/keeper/signers_test.go index 859027f97e..36b92ea29c 100644 --- a/x/metadata/keeper/signers_test.go +++ b/x/metadata/keeper/signers_test.go @@ -12,10 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - sdkmath "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -25,11 +22,16 @@ import ( simapp "github.com/provenance-io/provenance/app" "github.com/provenance-io/provenance/internal/pioconfig" + "github.com/provenance-io/provenance/testutil/assertions" markertypes "github.com/provenance-io/provenance/x/marker/types" "github.com/provenance-io/provenance/x/metadata/keeper" "github.com/provenance-io/provenance/x/metadata/types" ) +func TestAuthzTestSuite(t *testing.T) { + suite.Run(t, new(AuthzTestSuite)) +} + type AuthzTestSuite struct { suite.Suite @@ -70,18 +72,7 @@ func (s *AuthzTestSuite) SetupTest() { } func (s *AuthzTestSuite) FreshCtx() sdk.Context { - return keeper.AddAuthzCacheToContext(s.app.BaseApp.NewContextLegacy(false, cmtproto.Header{Time: time.Now()})) -} - -// AssertErrorValue asserts that: -// - If errorString is empty, theError must be nil -// - If errorString is not empty, theError must equal the errorString. -func AssertErrorValue(t *testing.T, theError error, errorString string, msgAndArgs ...interface{}) bool { - t.Helper() - if len(errorString) > 0 { - return assert.EqualError(t, theError, errorString, msgAndArgs...) - } - return assert.NoError(t, theError, msgAndArgs...) + return FreshCtx(s.app).WithBlockTime(time.Now()) } // AssertErrorValue asserts that: @@ -89,11 +80,33 @@ func AssertErrorValue(t *testing.T, theError error, errorString string, msgAndAr // - If errorString is not empty, theError must equal the errorString. func (s *AuthzTestSuite) AssertErrorValue(theError error, errorString string, msgAndArgs ...interface{}) bool { s.T().Helper() - return AssertErrorValue(s.T(), theError, errorString, msgAndArgs...) + return assertions.AssertErrorValue(s.T(), theError, errorString, msgAndArgs...) } -func TestAuthzTestSuite(t *testing.T) { - suite.Run(t, new(AuthzTestSuite)) +// partiesCopy creates a new []*types.PartyDetails with copies of each provided entry. +// Nil in = nil out. +func partiesCopy(parties []*types.PartyDetails) []*types.PartyDetails { + if parties == nil { + return nil + } + rv := make([]*types.PartyDetails, len(parties)) + for i, party := range parties { + rv[i] = party.Copy() + } + return rv +} + +// partiesReversed creates a new []*types.PartyDetails with copies of each provided entry +// in the opposite order as provided. Nil in = nil out. +func partiesReversed(parties []*types.PartyDetails) []*types.PartyDetails { + if parties == nil { + return nil + } + rv := make([]*types.PartyDetails, len(parties)) + for i, party := range parties { + rv[len(rv)-i-1] = party.Copy() + } + return rv } func (s *AuthzTestSuite) TestWriteScopeSmartContractValueOwnerAuthz() { @@ -161,15 +174,17 @@ func (s *AuthzTestSuite) TestWriteScopeSmartContractValueOwnerAuthz() { name string existing *types.Scope msg *types.MsgWriteScopeRequest - exp string + expAddrs []sdk.AccAddress + expErr string }{ { // Alice makes a call to the Sam that causes Sam to try to change Bob's Scope's value owner to Alice. - // That must fail. + // This is why it's a bad idea to authz grant a smart contract. We allow this since both Alice and Bob + // have authz granted the smart contract to be able to so. name: "smart contract updating value owner of wrong scope", existing: newScope(addrBob), msg: newMsg(addrAlice, addrSam.String(), addrAlice.String()), - exp: "missing signature from existing value owner " + addrBob.String(), + expAddrs: []sdk.AccAddress{addrSam}, }, { // Bob makes a call to Sam to do stuff that causes Sam to try to change Alice's Scope's value owner to Bob. @@ -177,28 +192,28 @@ func (s *AuthzTestSuite) TestWriteScopeSmartContractValueOwnerAuthz() { name: "smart contract updating value owner of other scope but invoker is authorized", existing: newScope(addrAlice), msg: newMsg(addrBob, addrSam.String(), addrBob.String()), - exp: "", + expAddrs: []sdk.AccAddress{addrSam}, }, { // If the value owner is the smart contract, it can be updated to Alice by Alice invoking the smart contract name: "value owner is smart contract updating to invoker", existing: newScope(addrSam), msg: newMsg(addrAlice, addrSam.String(), addrAlice.String()), - exp: "", + expAddrs: []sdk.AccAddress{addrSam}, }, { // If the value owner is the smart contract, it can be updated to Bob by Alice invoking the smart contract name: "value owner is smart contract updating to non-invoker", existing: newScope(addrSam), msg: newMsg(addrBob, addrSam.String(), addrAlice.String()), - exp: "", + expAddrs: []sdk.AccAddress{addrSam}, }, { // If the value owner is the smart contract, it can be updated to Alice by Bob invoking the smart contract name: "value owner is smart contract updating to non-invoker with authz", existing: newScope(addrSam), msg: newMsg(addrAlice, addrSam.String(), addrBob.String()), - exp: "", + expAddrs: []sdk.AccAddress{addrSam}, }, } @@ -207,8 +222,18 @@ func (s *AuthzTestSuite) TestWriteScopeSmartContractValueOwnerAuthz() { authKeeper.ClearResults() authzKeeper.ClearResults() - err := mdKeeper.ValidateWriteScope(s.FreshCtx(), tc.existing, tc.msg) - s.AssertErrorValue(err, tc.exp, "ValidateWriteScope") + if tc.existing != nil { + defer WriteTempScope(s.T(), s.app.MetadataKeeper, s.FreshCtx(), *tc.existing)() + } + + var addrs []sdk.AccAddress + var err error + testFunc := func() { + addrs, err = mdKeeper.ValidateWriteScope(s.FreshCtx(), tc.msg) + } + s.Require().NotPanics(testFunc, "ValidateWriteScope") + s.AssertErrorValue(err, tc.expErr, "error from ValidateWriteScope") + s.Assert().Equal(tc.expAddrs, addrs, "addresses from ValidateWriteScope") }) } } @@ -477,7 +502,7 @@ func (s *AuthzTestSuite) TestValidateAllRequiredPartiesSigned() { msg types.MetadataMsg authKeeper *MockAuthKeeper authzKeeper *MockAuthzKeeper - expParties []*keeper.PartyDetails + expParties []*types.PartyDetails expErr string }{ { @@ -488,7 +513,7 @@ func (s *AuthzTestSuite) TestValidateAllRequiredPartiesSigned() { msg: newMsg("signer1"), authKeeper: NewMockAuthKeeper(), authzKeeper: NewMockAuthzKeeper(), - expParties: []*keeper.PartyDetails{}, + expParties: []*types.PartyDetails{}, expErr: "", }, { @@ -620,8 +645,8 @@ func (s *AuthzTestSuite) TestValidateAllRequiredPartiesSigned() { msg: newMsg("party1"), authKeeper: NewMockAuthKeeper(), authzKeeper: NewMockAuthzKeeper(), - expParties: []*keeper.PartyDetails{ - keeper.TestablePartyDetails{ + expParties: []*types.PartyDetails{ + types.TestablePartyDetails{ Address: accStr("party1"), Role: provenance, Optional: false, @@ -644,8 +669,8 @@ func (s *AuthzTestSuite) TestValidateAllRequiredPartiesSigned() { NewGetAccountCall(acc("party1"), scAcct("party1")), ), authzKeeper: NewMockAuthzKeeper(), - expParties: []*keeper.PartyDetails{ - keeper.TestablePartyDetails{ + expParties: []*types.PartyDetails{ + types.TestablePartyDetails{ Address: accStr("party1"), Role: owner, Optional: false, @@ -723,8 +748,8 @@ func (s *AuthzTestSuite) TestValidateAllRequiredPartiesSigned_CountAuthorization }, } reqRoles := []types.PartyType{types.PartyType_PARTY_TYPE_VALIDATOR} - expDetails := []*keeper.PartyDetails{ - keeper.TestablePartyDetails{ + expDetails := []*types.PartyDetails{ + types.TestablePartyDetails{ Address: accStr(party1), Role: types.PartyType_PARTY_TYPE_VALIDATOR, Optional: true, @@ -734,7 +759,7 @@ func (s *AuthzTestSuite) TestValidateAllRequiredPartiesSigned_CountAuthorization CanBeUsedBySpec: true, UsedBySpec: true, }.Real(), - keeper.TestablePartyDetails{ + types.TestablePartyDetails{ Address: accStr(party1), Role: types.PartyType_PARTY_TYPE_OWNER, Optional: false, @@ -744,7 +769,7 @@ func (s *AuthzTestSuite) TestValidateAllRequiredPartiesSigned_CountAuthorization CanBeUsedBySpec: false, UsedBySpec: false, }.Real(), - keeper.TestablePartyDetails{ + types.TestablePartyDetails{ Address: accStr(party2), Role: types.PartyType_PARTY_TYPE_SERVICER, Optional: false, @@ -786,8 +811,8 @@ func (s *AuthzTestSuite) TestValidateAllRequiredPartiesSigned_CountAuthorization func TestAssociateSigners(t *testing.T) { // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(address string, acc sdk.AccAddress, signer string, signerAcc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pd := func(address string, acc sdk.AccAddress, signer string, signerAcc sdk.AccAddress) *types.PartyDetails { + return types.TestablePartyDetails{ Address: address, Acc: acc, Signer: signer, @@ -795,8 +820,8 @@ func TestAssociateSigners(t *testing.T) { }.Real() } // pdz is a shorter varargs way to define a []*keeper.PartyDetails. - pdz := func(parties ...*keeper.PartyDetails) []*keeper.PartyDetails { - rv := make([]*keeper.PartyDetails, 0, len(parties)) + pdz := func(parties ...*types.PartyDetails) []*types.PartyDetails { + rv := make([]*types.PartyDetails, 0, len(parties)) rv = append(rv, parties...) return rv } @@ -820,11 +845,11 @@ func TestAssociateSigners(t *testing.T) { } // partyStr gets a string of the golang code that would make the provided party for these tests. - partyStr := func(p *keeper.PartyDetails) string { + partyStr := func(p *types.PartyDetails) string { if p == nil { return "nil" } - party := p.Testable() + party := types.NewTestablePartyDetails(p) var addrVal string addrAcc, err := sdk.AccAddressFromBech32(party.Address) if err == nil { @@ -850,7 +875,7 @@ func TestAssociateSigners(t *testing.T) { return fmt.Sprintf("pd(%s, %s, %s, %s)", addrVal, accVal, sigVal, sigAccVal) } // partiesStr gets a string of the golang code that would make the provided parties for these tests. - partiesStr := func(parties []*keeper.PartyDetails) string { + partiesStr := func(parties []*types.PartyDetails) string { if parties == nil { return "nil" } @@ -905,16 +930,16 @@ func TestAssociateSigners(t *testing.T) { // Both parties and expParties must have the same length and if one is nil, the other must be too. // The entries are shuffled in tandem. E.g. if parties becomes [1, 0, 2] then expParties will also have the order [1, 0, 2]. // Nil in = nil out. - partiesShuffled := func(r *rand.Rand, parties, expParties []*keeper.PartyDetails) ([]*keeper.PartyDetails, []*keeper.PartyDetails) { + partiesShuffled := func(r *rand.Rand, parties, expParties []*types.PartyDetails) ([]*types.PartyDetails, []*types.PartyDetails) { if (parties == nil && expParties != nil) || (parties != nil && expParties == nil) || (len(parties) != len(expParties)) { panic("test definition failure: parties and expParties should always have the same number of entries") } if parties == nil { return nil, nil } - rvp := make([]*keeper.PartyDetails, 0, len(parties)) + rvp := make([]*types.PartyDetails, 0, len(parties)) rvp = append(rvp, parties...) - rve := make([]*keeper.PartyDetails, 0, len(expParties)) + rve := make([]*types.PartyDetails, 0, len(expParties)) rve = append(rve, expParties...) r.Shuffle(len(rve), func(i, j int) { rve[i], rve[j] = rve[j], rve[i] @@ -925,9 +950,9 @@ func TestAssociateSigners(t *testing.T) { type testCase struct { name string - parties []*keeper.PartyDetails + parties []*types.PartyDetails signers *keeper.SignersWrapper - expParties []*keeper.PartyDetails + expParties []*types.PartyDetails } tests := []testCase{ @@ -1246,8 +1271,8 @@ func TestAssociateSigners(t *testing.T) { func TestFindUnsignedRequired(t *testing.T) { // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(address string, optional bool, signer string, signerAcc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pd := func(address string, optional bool, signer string, signerAcc sdk.AccAddress) *types.PartyDetails { + return types.TestablePartyDetails{ Address: address, Optional: optional, Signer: signer, @@ -1255,8 +1280,8 @@ func TestFindUnsignedRequired(t *testing.T) { }.Real() } // pdz is just a shorter way to define a []*keeper.PartyDetails - pdz := func(parties ...*keeper.PartyDetails) []*keeper.PartyDetails { - rv := make([]*keeper.PartyDetails, 0, len(parties)) + pdz := func(parties ...*types.PartyDetails) []*types.PartyDetails { + rv := make([]*types.PartyDetails, 0, len(parties)) rv = append(rv, parties...) return rv } @@ -1266,8 +1291,8 @@ func TestFindUnsignedRequired(t *testing.T) { tests := []struct { name string - parties []*keeper.PartyDetails - exp []*keeper.PartyDetails + parties []*types.PartyDetails + exp []*types.PartyDetails }{ { name: "nil", @@ -1396,8 +1421,8 @@ func TestFindUnsignedRequired(t *testing.T) { func TestAssociateRequiredRoles(t *testing.T) { // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(role types.PartyType, canBeUsed, isUsed bool, signer string, signerAcc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pd := func(role types.PartyType, canBeUsed, isUsed bool, signer string, signerAcc sdk.AccAddress) *types.PartyDetails { + return types.TestablePartyDetails{ Role: role, Signer: signer, SignerAcc: signerAcc, @@ -1406,8 +1431,8 @@ func TestAssociateRequiredRoles(t *testing.T) { }.Real() } // pdz is just a shorter way to define a []*keeper.PartyDetails - pdz := func(parties ...*keeper.PartyDetails) []*keeper.PartyDetails { - rv := make([]*keeper.PartyDetails, 0, len(parties)) + pdz := func(parties ...*types.PartyDetails) []*types.PartyDetails { + rv := make([]*types.PartyDetails, 0, len(parties)) rv = append(rv, parties...) return rv } @@ -1441,10 +1466,10 @@ func TestAssociateRequiredRoles(t *testing.T) { type testCase struct { name string - parties []*keeper.PartyDetails + parties []*types.PartyDetails reqRoles []types.PartyType exp []types.PartyType - expParties []*keeper.PartyDetails + expParties []*types.PartyDetails } tests := []testCase{ @@ -1457,10 +1482,10 @@ func TestAssociateRequiredRoles(t *testing.T) { }, { name: "empty nil", - parties: []*keeper.PartyDetails{}, + parties: []*types.PartyDetails{}, reqRoles: nil, exp: nil, - expParties: []*keeper.PartyDetails{}, + expParties: []*types.PartyDetails{}, }, { name: "nil empty", @@ -1471,10 +1496,10 @@ func TestAssociateRequiredRoles(t *testing.T) { }, { name: "empty empty", - parties: []*keeper.PartyDetails{}, + parties: []*types.PartyDetails{}, reqRoles: []types.PartyType{}, exp: nil, - expParties: []*keeper.PartyDetails{}, + expParties: []*types.PartyDetails{}, }, { name: "2 req nil parties", @@ -1875,15 +1900,15 @@ func TestAssociateRequiredRoles(t *testing.T) { func TestMissingRolesString(t *testing.T) { // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(role types.PartyType, used bool) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pd := func(role types.PartyType, used bool) *types.PartyDetails { + return types.TestablePartyDetails{ Role: role, UsedBySpec: used, }.Real() } // pdz is just a shorter way to define a []*keeper.PartyDetails - pdz := func(parties ...*keeper.PartyDetails) []*keeper.PartyDetails { - rv := make([]*keeper.PartyDetails, 0, len(parties)) + pdz := func(parties ...*types.PartyDetails) []*types.PartyDetails { + rv := make([]*types.PartyDetails, 0, len(parties)) rv = append(rv, parties...) return rv } @@ -1926,8 +1951,8 @@ func TestMissingRolesString(t *testing.T) { return rv } // partiesForDeterministismTests returns two parties for each role (1 used, 1 not) in a random order. - partiesForDeterministismTests := func() []*keeper.PartyDetails { - var rv []*keeper.PartyDetails + partiesForDeterministismTests := func() []*types.PartyDetails { + var rv []*types.PartyDetails for i := range types.PartyType_name { role := types.PartyType(i) rv = append(rv, pd(role, true), pd(role, false)) @@ -1957,11 +1982,11 @@ func TestMissingRolesString(t *testing.T) { return fmt.Sprintf("rz(%s)", strings.Join(strs, ", ")) } // partyStr gets a string of the golang code that would make the provided party for these tests. - partyStr := func(party *keeper.PartyDetails) string { + partyStr := func(party *types.PartyDetails) string { return fmt.Sprintf("pd(%s, %t)", roleStr(party.GetRole()), party.IsUsed()) } // partiesStr gets a string of the golang code that would make the provided parties for these tests. - partiesStr := func(parties []*keeper.PartyDetails) string { + partiesStr := func(parties []*types.PartyDetails) string { if parties == nil { return "nil" } @@ -1999,11 +2024,11 @@ func TestMissingRolesString(t *testing.T) { return rv } // partiesShuffled copies each of the provided party and returns them in a random order. Nil in = nil out. - partiesShuffled := func(r *rand.Rand, parties []*keeper.PartyDetails) []*keeper.PartyDetails { + partiesShuffled := func(r *rand.Rand, parties []*types.PartyDetails) []*types.PartyDetails { if parties == nil { return nil } - rv := make([]*keeper.PartyDetails, 0, len(parties)) + rv := make([]*types.PartyDetails, 0, len(parties)) rv = append(rv, parties...) r.Shuffle(len(rv), func(i, j int) { rv[i], rv[j] = rv[j], rv[i] @@ -2013,7 +2038,7 @@ func TestMissingRolesString(t *testing.T) { type testCase struct { name string - parties []*keeper.PartyDetails + parties []*types.PartyDetails reqRoles []types.PartyType exp string } @@ -2430,17 +2455,15 @@ func TestGetAuthzMessageTypeURLs(t *testing.T) { } return url[lastDot+1:] } - getName := func(tc testCase) string { - if tc.name != "" { - return tc.name + newCase := func(url string, also ...string) testCase { + desc := " (boring)" + if len(also) > 0 { + desc = " (special)" } - return getMsgName(tc.url) - } - boringCase := func(url string) testCase { return testCase{ - name: "boring " + getMsgName(url), + name: getMsgName(url) + desc, url: url, - expected: []string{url}, + expected: append([]string{url}, also...), } } @@ -2455,66 +2478,33 @@ func TestGetAuthzMessageTypeURLs(t *testing.T) { url: "random", expected: []string{"random"}, }, - boringCase(types.TypeURLMsgWriteScopeRequest), - boringCase(types.TypeURLMsgDeleteScopeRequest), - { - url: types.TypeURLMsgAddScopeDataAccessRequest, - expected: []string{types.TypeURLMsgAddScopeDataAccessRequest, types.TypeURLMsgWriteScopeRequest}, - }, - { - url: types.TypeURLMsgDeleteScopeDataAccessRequest, - expected: []string{types.TypeURLMsgDeleteScopeDataAccessRequest, types.TypeURLMsgWriteScopeRequest}, - }, - { - url: types.TypeURLMsgAddScopeOwnerRequest, - expected: []string{types.TypeURLMsgAddScopeOwnerRequest, types.TypeURLMsgWriteScopeRequest}, - }, - { - url: types.TypeURLMsgDeleteScopeOwnerRequest, - expected: []string{types.TypeURLMsgDeleteScopeOwnerRequest, types.TypeURLMsgWriteScopeRequest}, - }, - { - url: types.TypeURLMsgUpdateValueOwnersRequest, - expected: []string{types.TypeURLMsgUpdateValueOwnersRequest, types.TypeURLMsgWriteScopeRequest}, - }, - { - url: types.TypeURLMsgMigrateValueOwnerRequest, - expected: []string{types.TypeURLMsgMigrateValueOwnerRequest, types.TypeURLMsgWriteScopeRequest}, - }, - boringCase(types.TypeURLMsgWriteSessionRequest), - { - url: types.TypeURLMsgWriteRecordRequest, - expected: []string{types.TypeURLMsgWriteRecordRequest, types.TypeURLMsgWriteSessionRequest}, - }, - boringCase(types.TypeURLMsgDeleteRecordRequest), - boringCase(types.TypeURLMsgWriteScopeSpecificationRequest), - boringCase(types.TypeURLMsgDeleteScopeSpecificationRequest), - boringCase(types.TypeURLMsgWriteContractSpecificationRequest), - boringCase(types.TypeURLMsgDeleteContractSpecificationRequest), - { - url: types.TypeURLMsgAddContractSpecToScopeSpecRequest, - expected: []string{types.TypeURLMsgAddContractSpecToScopeSpecRequest, types.TypeURLMsgWriteScopeSpecificationRequest}, - }, - { - url: types.TypeURLMsgDeleteContractSpecFromScopeSpecRequest, - expected: []string{types.TypeURLMsgDeleteContractSpecFromScopeSpecRequest, types.TypeURLMsgWriteScopeSpecificationRequest}, - }, - { - url: types.TypeURLMsgWriteRecordSpecificationRequest, - expected: []string{types.TypeURLMsgWriteRecordSpecificationRequest, types.TypeURLMsgWriteContractSpecificationRequest}, - }, - { - url: types.TypeURLMsgDeleteRecordSpecificationRequest, - expected: []string{types.TypeURLMsgDeleteRecordSpecificationRequest, types.TypeURLMsgDeleteContractSpecificationRequest}, - }, - boringCase(types.TypeURLMsgBindOSLocatorRequest), - boringCase(types.TypeURLMsgDeleteOSLocatorRequest), - boringCase(types.TypeURLMsgModifyOSLocatorRequest), - boringCase(types.TypeURLMsgSetAccountDataRequest), + newCase(types.TypeURLMsgWriteScopeRequest), + newCase(types.TypeURLMsgDeleteScopeRequest), + newCase(types.TypeURLMsgAddScopeDataAccessRequest, types.TypeURLMsgWriteScopeRequest), + newCase(types.TypeURLMsgDeleteScopeDataAccessRequest, types.TypeURLMsgWriteScopeRequest), + newCase(types.TypeURLMsgAddScopeOwnerRequest, types.TypeURLMsgWriteScopeRequest), + newCase(types.TypeURLMsgDeleteScopeOwnerRequest, types.TypeURLMsgWriteScopeRequest), + newCase(types.TypeURLMsgUpdateValueOwnersRequest), + newCase(types.TypeURLMsgMigrateValueOwnerRequest), + newCase(types.TypeURLMsgWriteSessionRequest), + newCase(types.TypeURLMsgWriteRecordRequest, types.TypeURLMsgWriteSessionRequest), + newCase(types.TypeURLMsgDeleteRecordRequest), + newCase(types.TypeURLMsgWriteScopeSpecificationRequest), + newCase(types.TypeURLMsgDeleteScopeSpecificationRequest), + newCase(types.TypeURLMsgWriteContractSpecificationRequest), + newCase(types.TypeURLMsgDeleteContractSpecificationRequest), + newCase(types.TypeURLMsgAddContractSpecToScopeSpecRequest, types.TypeURLMsgWriteScopeSpecificationRequest), + newCase(types.TypeURLMsgDeleteContractSpecFromScopeSpecRequest, types.TypeURLMsgWriteScopeSpecificationRequest), + newCase(types.TypeURLMsgWriteRecordSpecificationRequest, types.TypeURLMsgWriteContractSpecificationRequest), + newCase(types.TypeURLMsgDeleteRecordSpecificationRequest, types.TypeURLMsgDeleteContractSpecificationRequest), + newCase(types.TypeURLMsgBindOSLocatorRequest), + newCase(types.TypeURLMsgDeleteOSLocatorRequest), + newCase(types.TypeURLMsgModifyOSLocatorRequest), + newCase(types.TypeURLMsgSetAccountDataRequest), } for _, tc := range tests { - t.Run(getName(tc), func(t *testing.T) { + t.Run(tc.name, func(t *testing.T) { actual := keeper.GetAuthzMessageTypeURLs(tc.url) assert.Equal(t, tc.expected, actual, "getAuthzMessageTypeURLs(%q)", tc.url) }) @@ -3144,23 +3134,23 @@ func (s *AuthzTestSuite) TestAssociateAuthorizations() { } // pd is a short way to create a *keeper.PartyDetails with the info needed in these tests. // The provided strings are passed through accStr. - pd := func(address, signer string) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pd := func(address, signer string) *types.PartyDetails { + return types.TestablePartyDetails{ Address: accStr(address), Signer: accStr(signer), }.Real() } // pde is pd "expected". It allows setting the addrAcc and signerAcc values too. - pde := func(address, addrAcc, signer, signerAcc string) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pde := func(address, addrAcc, signer, signerAcc string) *types.PartyDetails { + return types.TestablePartyDetails{ Address: accStr(address), Acc: acc(addrAcc), Signer: accStr(signer), SignerAcc: acc(signerAcc), }.Real() } - pdz := func(parties ...*keeper.PartyDetails) []*keeper.PartyDetails { - rv := make([]*keeper.PartyDetails, 0, len(parties)) + pdz := func(parties ...*types.PartyDetails) []*types.PartyDetails { + rv := make([]*types.PartyDetails, 0, len(parties)) rv = append(rv, parties...) return rv } @@ -3188,11 +3178,11 @@ func (s *AuthzTestSuite) TestAssociateAuthorizations() { tests := []struct { name string - parties []*keeper.PartyDetails + parties []*types.PartyDetails signers *keeper.SignersWrapper authzKeeper *MockAuthzKeeper expErr string - expParties []*keeper.PartyDetails + expParties []*types.PartyDetails expGetAuth []*GetAuthorizationCall }{ { @@ -3223,11 +3213,11 @@ func (s *AuthzTestSuite) TestAssociateAuthorizations() { }, { name: "1 party not bech32", - parties: pdz(keeper.TestablePartyDetails{Address: "not-correct"}.Real()), + parties: pdz(types.TestablePartyDetails{Address: "not-correct"}.Real()), signers: sw("signer1"), authzKeeper: NewMockAuthzKeeper(), expErr: "", - expParties: pdz(keeper.TestablePartyDetails{Address: "not-correct"}.Real()), + expParties: pdz(types.TestablePartyDetails{Address: "not-correct"}.Real()), expGetAuth: nil, }, { @@ -3445,8 +3435,8 @@ func (s *AuthzTestSuite) TestAssociateAuthorizations() { s.Run("onAssociation with counter", func() { counter := 0 - var partiesAssociated []*keeper.PartyDetails - onAssoc := func(party *keeper.PartyDetails) bool { + var partiesAssociated []*types.PartyDetails + onAssoc := func(party *types.PartyDetails) bool { counter++ partiesAssociated = append(partiesAssociated, party) return false @@ -3509,8 +3499,8 @@ func (s *AuthzTestSuite) TestAssociateAuthorizations() { s.Run("onAssociation stop early", func() { counter := 0 stopAt := 3 - var partiesAssociated []*keeper.PartyDetails - onAssoc := func(party *keeper.PartyDetails) bool { + var partiesAssociated []*types.PartyDetails + onAssoc := func(party *types.PartyDetails) bool { counter++ partiesAssociated = append(partiesAssociated, party) return counter >= stopAt @@ -3542,7 +3532,7 @@ func (s *AuthzTestSuite) TestAssociateAuthorizations() { GetAuthorizationCall{ GrantInfo: GrantInfo{ Grantee: acc("signer"), - Granter: sdk.MustAccAddressFromBech32(party.Testable().Address), + Granter: sdk.MustAccAddressFromBech32(types.NewTestablePartyDetails(party).Address), MsgType: theMsgType, }, Result: GetAuthorizationResult{ @@ -3588,8 +3578,8 @@ func (s *AuthzTestSuite) TestAssociateAuthorizationsForRoles() { } // pdu creates a usable, unsigned *keeper.PartyDetails. // The provided strings are passed through accStr. - pdu := func(address string, role types.PartyType) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pdu := func(address string, role types.PartyType) *types.PartyDetails { + return types.TestablePartyDetails{ Address: accStr(address), Acc: acc(address), Role: role, @@ -3598,8 +3588,8 @@ func (s *AuthzTestSuite) TestAssociateAuthorizationsForRoles() { }.Real() } // pdx creates a *keeper.PartyDetails that isn't usable. - pdx := func(address string, role types.PartyType) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pdx := func(address string, role types.PartyType) *types.PartyDetails { + return types.TestablePartyDetails{ Address: accStr(address), Acc: acc(address), Role: role, @@ -3608,8 +3598,8 @@ func (s *AuthzTestSuite) TestAssociateAuthorizationsForRoles() { }.Real() } // pdus creates a *keeper.PartyDetails that was usable but now has a signer and is used. - pdus := func(address string, role types.PartyType, signer string) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pdus := func(address string, role types.PartyType, signer string) *types.PartyDetails { + return types.TestablePartyDetails{ Address: accStr(address), Acc: acc(address), Role: role, @@ -3618,8 +3608,8 @@ func (s *AuthzTestSuite) TestAssociateAuthorizationsForRoles() { SignerAcc: acc(signer), }.Real() } - pdz := func(parties ...*keeper.PartyDetails) []*keeper.PartyDetails { - rv := make([]*keeper.PartyDetails, 0, len(parties)) + pdz := func(parties ...*types.PartyDetails) []*types.PartyDetails { + rv := make([]*types.PartyDetails, 0, len(parties)) rv = append(rv, parties...) return rv } @@ -3686,12 +3676,12 @@ func (s *AuthzTestSuite) TestAssociateAuthorizationsForRoles() { tests := []struct { name string roles []types.PartyType - parties []*keeper.PartyDetails + parties []*types.PartyDetails signers *keeper.SignersWrapper authzKeeper *MockAuthzKeeper expMissing bool expErr string - expParties []*keeper.PartyDetails + expParties []*types.PartyDetails expGetAuth []*GetAuthorizationCall }{ { @@ -4214,15 +4204,15 @@ func (s *AuthzTestSuite) TestValidateProvenanceRole() { accStr := func(addr string) string { return acc(addr).String() } - pd := func(canBeUsed bool, role types.PartyType, address string) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + pd := func(canBeUsed bool, role types.PartyType, address string) *types.PartyDetails { + return types.TestablePartyDetails{ CanBeUsedBySpec: canBeUsed, Role: role, Address: address, }.Real() } - pdz := func(parties ...*keeper.PartyDetails) []*keeper.PartyDetails { - rv := make([]*keeper.PartyDetails, 0, len(parties)) + pdz := func(parties ...*types.PartyDetails) []*types.PartyDetails { + rv := make([]*types.PartyDetails, 0, len(parties)) rv = append(rv, parties...) return rv } @@ -4272,7 +4262,7 @@ func (s *AuthzTestSuite) TestValidateProvenanceRole() { tests := []struct { name string - parties []*keeper.PartyDetails + parties []*types.PartyDetails authKeeper *MockAuthKeeper expErr string expGetAcc []*GetAccountCall @@ -4718,7 +4708,7 @@ func (s *AuthzTestSuite) TestIsWasmAccount() { authKeeper.ClearResults() // Make sure they're cached. - cache := keeper.GetAuthzCache(ctx) + cache := types.GetAuthzCache(ctx) for _, tc := range isWasmTests { hasEntry := cache.HasIsWasm(tc.addr) s.Assert().True(hasEntry, "cache HasIsWasm(%s)", tc.name) @@ -4802,7 +4792,7 @@ func (s *AuthzTestSuite) TestValidateSmartContractSigners() { tests := []struct { name string - usedSigners keeper.UsedSignersMap + usedSigners types.UsedSignersMap msg types.MetadataMsg authK *MockAuthKeeper authzK *MockAuthzKeeper @@ -5060,926 +5050,554 @@ func (s *AuthzTestSuite) TestValidateSmartContractSigners() { } } -func (s *AuthzTestSuite) TestValidateScopeValueOwnerUpdate() { - acc := func(addr string) sdk.AccAddress { - return sdk.AccAddress(addr) +func (s *AuthzTestSuite) TestValidateScopeValueOwnersSigners() { + addr1 := sdk.AccAddress("1addr_______________") // cosmos1x9skgerjta047h6lta047h6lta047h6l4429yc + addr2 := sdk.AccAddress("2addr_______________") // cosmos1xfskgerjta047h6lta047h6lta047h6lh0rr9a + addr3 := sdk.AccAddress("3addr_______________") // cosmos1xdskgerjta047h6lta047h6lta047h6lw7ypa7 + addr4 := sdk.AccAddress("4addr_______________") // cosmos1x3skgerjta047h6lta047h6lta047h6lnj308h + addr5 := sdk.AccAddress("5addr_______________") // cosmos1x4skgerjta047h6lta047h6lta047h6l2rkdl5 + addr6 := sdk.AccAddress("6addr_______________") // cosmos1xeskgerjta047h6lta047h6lta047h6lgelt73 + + type msgMaker struct { + name string + msgType string + make func(signers []sdk.AccAddress) types.MetadataMsg } - accStr := func(addr string) string { - return acc(addr).String() + msgMakerUpdate := msgMaker{ + name: "update", + msgType: types.TypeURLMsgUpdateValueOwnersRequest, + make: func(signers []sdk.AccAddress) types.MetadataMsg { + return &types.MsgUpdateValueOwnersRequest{Signers: mapToStrings(signers)} + }, } - - withdrawAddrAcc := acc("withdraw_address____") - noWithdrawAddrAcc := acc("no_withdraw_address_") - depositAddrAcc := acc("deposit_address_____") - noDepositAddrAcc := acc("no_deposit_address__") - allAddrAcc := acc("all_address_________") - noneAddrAcc := acc("none_address________") - - withdrawAddr := withdrawAddrAcc.String() - noWithdrawAddr := noWithdrawAddrAcc.String() - depositAddr := depositAddrAcc.String() - noDepositAddr := noDepositAddrAcc.String() - allAddr := allAddrAcc.String() - noneAddr := noneAddrAcc.String() - - marker1 := &markertypes.MarkerAccount{ - BaseAccount: &authtypes.BaseAccount{}, - Manager: "", - AccessControl: []markertypes.AccessGrant{ - {Address: withdrawAddr, Permissions: markertypes.AccessList{markertypes.Access_Withdraw}}, - { - Address: noWithdrawAddr, - Permissions: markertypes.AccessList{ - markertypes.Access_Mint, markertypes.Access_Burn, - markertypes.Access_Deposit, - markertypes.Access_Delete, markertypes.Access_Admin, markertypes.Access_Transfer, - }, - }, - {Address: depositAddr, Permissions: markertypes.AccessList{markertypes.Access_Deposit}}, - { - Address: noDepositAddr, - Permissions: markertypes.AccessList{ - markertypes.Access_Mint, markertypes.Access_Burn, - markertypes.Access_Withdraw, - markertypes.Access_Delete, markertypes.Access_Admin, markertypes.Access_Transfer, - }, - }, - { - Address: allAddr, - Permissions: markertypes.AccessList{ - markertypes.Access_Mint, markertypes.Access_Burn, - markertypes.Access_Deposit, markertypes.Access_Withdraw, - markertypes.Access_Delete, markertypes.Access_Admin, markertypes.Access_Transfer, - }, - }, + msgMakerMigrate := msgMaker{ + name: "migrate", + msgType: types.TypeURLMsgMigrateValueOwnerRequest, + make: func(signers []sdk.AccAddress) types.MetadataMsg { + return &types.MsgMigrateValueOwnerRequest{Signers: mapToStrings(signers)} }, - Status: markertypes.StatusActive, - Denom: "onecoin", - Supply: sdkmath.OneInt(), - MarkerType: markertypes.MarkerType_RestrictedCoin, - } - marker1AddrAcc, marker1AddrErr := markertypes.MarkerAddress(marker1.Denom) - s.Require().NoError(marker1AddrErr, "MarkerAddress(%q)", marker1.Denom) - marker1.BaseAccount.Address = marker1AddrAcc.String() - marker1Addr := marker1AddrAcc.String() - - marker2 := &markertypes.MarkerAccount{ - BaseAccount: &authtypes.BaseAccount{}, - Manager: "", - AccessControl: marker1.AccessControl, - Status: markertypes.StatusActive, - Denom: "twocoin", - Supply: sdkmath.OneInt(), - MarkerType: markertypes.MarkerType_RestrictedCoin, - } - marker2AddrAcc, marker2AddrErr := markertypes.MarkerAddress(marker2.Denom) - s.Require().NoError(marker2AddrErr, "MarkerAddress(%q)", marker2.Denom) - marker2.BaseAccount.Address = marker2AddrAcc.String() - marker2Addr := marker2AddrAcc.String() - - mockAuthWithMarkers := func() *MockAuthKeeper { - return NewMockAuthKeeper().WithGetAccountResults( - NewGetAccountCall(marker1AddrAcc, marker1), - NewGetAccountCall(marker2AddrAcc, marker2), - ) } - - normalMsg := func(signers ...string) types.MetadataMsg { - rv := &types.MsgWriteScopeRequest{ - Signers: make([]string, 0, len(signers)), - } - rv.Signers = append(rv.Signers, signers...) - return rv + msgMakerWriteScope := msgMaker{ + name: "write", + msgType: types.TypeURLMsgWriteScopeRequest, + make: func(signers []sdk.AccAddress) types.MetadataMsg { + return &types.MsgWriteScopeRequest{Signers: mapToStrings(signers)} + }, } - normalMsgType := types.TypeURLMsgWriteScopeRequest - - errMissingSigRem := func(marker *markertypes.MarkerAccount) string { - return fmt.Sprintf("missing signature for %s (%s) with authority to withdraw/remove it as scope value owner", marker.Address, marker.Denom) + msgMakerDeleteScope := msgMaker{ + name: "delete", + msgType: types.TypeURLMsgDeleteScopeRequest, + make: func(signers []sdk.AccAddress) types.MetadataMsg { + return &types.MsgDeleteScopeRequest{Signers: mapToStrings(signers)} + }, } - errMissingSigAdd := func(marker *markertypes.MarkerAccount) string { - return fmt.Sprintf("missing signature for %s (%s) with authority to deposit/add it as scope value owner", marker.Address, marker.Denom) + allMsgMakers := []msgMaker{msgMakerUpdate, msgMakerMigrate, msgMakerWriteScope, msgMakerDeleteScope} + + missingSig := func(addr sdk.AccAddress) string { + return "missing signature from existing value owner \"" + addr.String() + "\"" } - errMissingSig := func(addr string) string { - return fmt.Sprintf("missing signature from existing value owner %s", addr) + + injectedErrorStr := "just some injected error message" + authzErr := func(addr sdk.AccAddress) string { + return "authz error with existing value owner \"" + addr.String() + "\": " + injectedErrorStr } tests := []struct { - name string - existing string - proposed string - msg types.MetadataMsg - authKeeper *MockAuthKeeper - authzKeeper *MockAuthzKeeper - expErr string - expUsed keeper.UsedSignersMap - expGetAccount []*GetAccountCall - expGetAuth []*GetAuthorizationCall + name string + msgMakers []msgMaker + authzGrants []GrantInfo // The MsgType field will be populated appropriately if not defined. + authzErrs []GrantInfo // The MsgType field will be populated appropriately if not defined. + wasmAddrs []sdk.AccAddress + markerAddrs []sdk.AccAddress + existingOwners []sdk.AccAddress + proposed string + signers []sdk.AccAddress + expAddrs []sdk.AccAddress + expUsedSigners types.UsedSignersMap + expErr string + expIsMarkerCalls []sdk.AccAddress }{ { - name: "both empty", - existing: "", - proposed: "", - expErr: "", - }, - { - name: "existing equals proposed", - existing: "same", - proposed: "same", - expErr: "", - }, - { - name: "empty to non-marker", - existing: "", - proposed: accStr("new-proposed"), - msg: normalMsg(), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expGetAccount: []*GetAccountCall{NewGetAccountCall(acc("new-proposed"), nil)}, - }, - { - name: "empty to non-bech32", - existing: "", - proposed: "proposed value owner string", - msg: normalMsg(), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expGetAccount: []*GetAccountCall{}, - }, - { - name: "empty to marker no signers", - existing: "", - proposed: marker2Addr, - msg: normalMsg(), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigAdd(marker2), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker2AddrAcc, marker2)}, - }, - { - name: "empty to marker 1 signer only withdraw permission", - existing: "", - proposed: marker2Addr, - msg: normalMsg(withdrawAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigAdd(marker2), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker2AddrAcc, marker2)}, - }, - { - name: "empty to marker 1 signer only deposit permission", - existing: "", - proposed: marker2Addr, - msg: normalMsg(depositAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{depositAddr: true}, - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker2AddrAcc, marker2)}, - }, - { - name: "empty to marker 1 signer all permissions except deposit", - existing: "", - proposed: marker2Addr, - msg: normalMsg(noDepositAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigAdd(marker2), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker2AddrAcc, marker2)}, - }, - { - name: "empty to marker 1 signer all permissions", - existing: "", - proposed: marker2Addr, - msg: normalMsg(allAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{allAddr: true}, - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker2AddrAcc, marker2)}, - }, - { - name: "empty to marker three signers none with deposit", - existing: "", - proposed: marker2Addr, - msg: normalMsg(noneAddr, accStr("some_other_addr"), noDepositAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigAdd(marker2), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker2AddrAcc, marker2)}, - }, - { - name: "empty to marker three signers one with deposit", - existing: "", - proposed: marker2Addr, - msg: normalMsg(noneAddr, noDepositAddr, depositAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{depositAddr: true}, - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker2AddrAcc, marker2)}, - }, - { - name: "marker to empty no signers", - existing: marker1Addr, - proposed: "", - msg: normalMsg(), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigRem(marker1), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to empty 1 signer only withdraw permission", - existing: marker1Addr, - proposed: "", - msg: normalMsg(withdrawAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{withdrawAddr: true}, - - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to empty 1 signer only deposit permission", - existing: marker1Addr, - proposed: "", - msg: normalMsg(depositAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigRem(marker1), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to empty 1 signer all permissions except withdraw", - existing: marker1Addr, - proposed: "", - msg: normalMsg(noWithdrawAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigRem(marker1), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to empty 1 signer all permissions", - existing: marker1Addr, - proposed: "", - msg: normalMsg(allAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{allAddr: true}, - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to empty three signers none with withdraw", - existing: marker1Addr, - proposed: "", - msg: normalMsg(noneAddr, accStr("some_other_addr"), noWithdrawAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigRem(marker1), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to empty three signers one with withdraw", - existing: marker1Addr, - proposed: "", - msg: normalMsg(noneAddr, noWithdrawAddr, withdrawAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{withdrawAddr: true}, - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to marker no signers", - existing: marker1Addr, - proposed: marker2Addr, - msg: normalMsg(), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigRem(marker1), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to marker 1 signer no permissions", - existing: marker1Addr, - proposed: marker2Addr, - msg: normalMsg(noneAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigRem(marker1), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to marker 1 signer only deposit permission", - existing: marker1Addr, - proposed: marker2Addr, - msg: normalMsg(depositAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigRem(marker1), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker1AddrAcc, marker1)}, - }, - { - name: "marker to marker 1 signer only withdraw permission", - existing: marker1Addr, - proposed: marker2Addr, - msg: normalMsg(withdrawAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigAdd(marker2), - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(marker1AddrAcc, marker1), - NewGetAccountCall(marker2AddrAcc, marker2), - }, + name: "nil existing owners", + existingOwners: nil, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr1}, + expAddrs: []sdk.AccAddress{addr1}, + expUsedSigners: types.NewUsedSignersMap(), + expErr: "", + expIsMarkerCalls: nil, }, { - name: "marker to marker 1 signer all permissions", - existing: marker1Addr, - proposed: marker2Addr, - msg: normalMsg(allAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{allAddr: true}, - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(marker1AddrAcc, marker1), - NewGetAccountCall(marker2AddrAcc, marker2), - }, + name: "empty existing owners", + existingOwners: []sdk.AccAddress{}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr1}, + expAddrs: []sdk.AccAddress{addr1}, + expUsedSigners: types.NewUsedSignersMap(), + expErr: "", + expIsMarkerCalls: nil, }, { - name: "marker to marker 2 signers only deposit then only withdraw", - existing: marker1Addr, - proposed: marker2Addr, - msg: normalMsg(depositAddr, withdrawAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{depositAddr: true, withdrawAddr: true}, - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(marker1AddrAcc, marker1), - NewGetAccountCall(marker2AddrAcc, marker2), - }, + name: "one existing owner that equals proposed", + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr1.String(), + signers: []sdk.AccAddress{addr3}, + expAddrs: nil, + expUsedSigners: types.NewUsedSignersMap(), + expErr: "", + expIsMarkerCalls: nil, }, { - name: "marker to marker 2 signers only withdraw then only deposit", - existing: marker1Addr, - proposed: marker2Addr, - msg: normalMsg(withdrawAddr, depositAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{withdrawAddr: true, depositAddr: true}, - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(marker1AddrAcc, marker1), - NewGetAccountCall(marker2AddrAcc, marker2), - }, + name: "no msg signers", + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr6.String(), + expErr: "missing signature from existing value owner \"" + addr1.String() + "\"", + expIsMarkerCalls: []sdk.AccAddress{addr1}, }, { - name: "marker to marker 3 signers one with withdraw one with deposit one with nothing", - existing: marker1Addr, - proposed: marker2Addr, - msg: normalMsg(withdrawAddr, noneAddr, depositAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{withdrawAddr: true, depositAddr: true}, - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(marker1AddrAcc, marker1), - NewGetAccountCall(marker2AddrAcc, marker2), - }, + name: "different signer from existing value owner", + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr2.String(), + signers: []sdk.AccAddress{addr2}, + expErr: missingSig(addr1), + expIsMarkerCalls: []sdk.AccAddress{addr1}, }, { - name: "marker to non-marker 1 signer only withdraw", - existing: marker2Addr, - proposed: accStr("something_else"), - msg: normalMsg(withdrawAddr), - authKeeper: mockAuthWithMarkers(), - expErr: "", - expUsed: map[string]bool{withdrawAddr: true}, - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(marker2AddrAcc, marker2), - NewGetAccountCall(acc("something_else"), nil), - }, + name: "first signer is wasm second is existing value owner", + wasmAddrs: []sdk.AccAddress{addr2}, + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr2, addr1}, + expErr: missingSig(addr1), + expIsMarkerCalls: []sdk.AccAddress{addr1}, }, { - name: "marker to non-marker 1 signer no withdraw", - existing: marker2Addr, - proposed: accStr("something_else"), - msg: normalMsg(noWithdrawAddr), - authKeeper: mockAuthWithMarkers(), - expErr: errMissingSigRem(marker2), - expGetAccount: []*GetAccountCall{NewGetAccountCall(marker2AddrAcc, marker2)}, - }, - { - name: "non-bech32 to empty in signers somehow", - existing: "existing_value_owner_string", - proposed: "", - msg: normalMsg(noneAddr, allAddr, "existing_value_owner_string", depositAddr), - authKeeper: NewMockAuthKeeper(), - expErr: "", - expUsed: map[string]bool{"existing_value_owner_string": true}, - expGetAccount: []*GetAccountCall{}, - }, - { - name: "non-bech32 to empty not in signers", - existing: "existing_value_owner_string", - proposed: "", - msg: normalMsg(noneAddr, allAddr, depositAddr), - authKeeper: NewMockAuthKeeper(), - authzKeeper: NewMockAuthzKeeper(), - expErr: errMissingSig("existing_value_owner_string"), - expGetAccount: []*GetAccountCall{}, - expGetAuth: []*GetAuthorizationCall{}, - }, - { - name: "addr to empty in signers", - existing: accStr("existing"), - proposed: "", - msg: normalMsg(accStr("existing")), - authKeeper: NewMockAuthKeeper(), - authzKeeper: NewMockAuthzKeeper(), - expErr: "", - expUsed: map[string]bool{accStr("existing"): true}, - expGetAccount: []*GetAccountCall{NewGetAccountCall(acc("existing"), nil)}, - expGetAuth: []*GetAuthorizationCall{}, - }, - { - name: "addr to other in signers", - existing: accStr("existing"), - proposed: accStr("proposed"), - msg: normalMsg(accStr("existing")), - authKeeper: NewMockAuthKeeper(), - authzKeeper: NewMockAuthzKeeper(), - expErr: "", - expUsed: map[string]bool{accStr("existing"): true}, - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(acc("existing"), nil), - NewGetAccountCall(acc("proposed"), nil), - }, - expGetAuth: []*GetAuthorizationCall{}, + name: "only signer is existing value owner", + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr2.String(), + signers: []sdk.AccAddress{addr1}, + expAddrs: []sdk.AccAddress{addr1}, + expUsedSigners: types.NewUsedSignersMap().Use(addr1.String()), }, { - name: "addr to other not in signer", - existing: accStr("existing"), - proposed: accStr("proposed"), - msg: normalMsg(noneAddr, allAddr, depositAddr), - authKeeper: NewMockAuthKeeper(), - authzKeeper: NewMockAuthzKeeper(), - expErr: errMissingSig(accStr("existing")), - expUsed: nil, - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(acc("existing"), nil), - NewGetAccountCall(noneAddrAcc, nil), - NewGetAccountCall(allAddrAcc, nil), - NewGetAccountCall(depositAddrAcc, nil), - }, - expGetAuth: []*GetAuthorizationCall{ - NewNotFoundGetAuthorizationCall(noneAddrAcc, acc("existing"), normalMsgType), - NewNotFoundGetAuthorizationCall(allAddrAcc, acc("existing"), normalMsgType), - NewNotFoundGetAuthorizationCall(depositAddrAcc, acc("existing"), normalMsgType), - }, + name: "only signer is wasm and is also existing value owner", + wasmAddrs: []sdk.AccAddress{addr1}, + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr2.String(), + signers: []sdk.AccAddress{addr1}, + expAddrs: []sdk.AccAddress{addr1}, + expUsedSigners: types.NewUsedSignersMap().Use(addr1.String()), }, { - name: "addr to empty with authz", - existing: accStr("existing"), - proposed: "", - msg: normalMsg(allAddr, noneAddr, depositAddr), - authKeeper: NewMockAuthKeeper(), - authzKeeper: NewMockAuthzKeeper().WithGetAuthorizationResults( - GetAuthorizationCall{ - GrantInfo: GrantInfo{Grantee: noneAddrAcc, Granter: acc("existing"), MsgType: normalMsgType}, - Result: GetAuthorizationResult{ - Auth: NewMockAuthorization("one", authz.AcceptResponse{Accept: true}, nil), - Exp: nil, - }, - }, - ), - expErr: "", - expUsed: map[string]bool{noneAddr: true}, - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(acc("existing"), nil), - NewGetAccountCall(allAddrAcc, nil), - NewGetAccountCall(noneAddrAcc, nil), - NewGetAccountCall(depositAddrAcc, nil), + name: "only signer is wasm with authz grants from existing value owners", + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr5}, + {Granter: addr2, Grantee: addr5}, + {Granter: addr3, Grantee: addr5}, + {Granter: addr4, Grantee: addr5}, }, - expGetAuth: []*GetAuthorizationCall{ - { - GrantInfo: GrantInfo{Grantee: allAddrAcc, Granter: acc("existing"), MsgType: normalMsgType}, - Result: GetAuthorizationResult{Auth: nil, Exp: nil}, - }, - { - GrantInfo: GrantInfo{Grantee: noneAddrAcc, Granter: acc("existing"), MsgType: normalMsgType}, - Result: GetAuthorizationResult{ - Auth: NewMockAuthorization("one", - authz.AcceptResponse{Accept: true}, - nil).WithAcceptCalls(normalMsg(allAddr, noneAddr, depositAddr)), - Exp: nil, - }, - }, - }, - }, - { - name: "addr to empty not authorized", - existing: accStr("existing"), - proposed: "", - msg: normalMsg(allAddr, withdrawAddr, depositAddr), - authKeeper: NewMockAuthKeeper(), - authzKeeper: NewMockAuthzKeeper(), - expErr: errMissingSig(accStr("existing")), - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(acc("existing"), nil), - NewGetAccountCall(allAddrAcc, nil), - NewGetAccountCall(withdrawAddrAcc, nil), - NewGetAccountCall(depositAddrAcc, nil), + wasmAddrs: []sdk.AccAddress{addr5}, + existingOwners: []sdk.AccAddress{addr1, addr2, addr3, addr4}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr5}, + expAddrs: []sdk.AccAddress{addr5}, + expUsedSigners: types.NewUsedSignersMap().Use(addr5.String()), + expIsMarkerCalls: []sdk.AccAddress{addr1, addr2, addr3, addr4}, + }, + { + name: "only signer is wasm with authz grants from all but one existing value owner", + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr5}, + {Granter: addr2, Grantee: addr5}, + {Granter: addr4, Grantee: addr5}, }, - expGetAuth: []*GetAuthorizationCall{ - { - GrantInfo: GrantInfo{Grantee: allAddrAcc, Granter: acc("existing"), MsgType: normalMsgType}, - Result: GetAuthorizationResult{Auth: nil, Exp: nil}, - }, - { - GrantInfo: GrantInfo{Grantee: withdrawAddrAcc, Granter: acc("existing"), MsgType: normalMsgType}, - Result: GetAuthorizationResult{Auth: nil, Exp: nil}, - }, - { - GrantInfo: GrantInfo{Grantee: depositAddrAcc, Granter: acc("existing"), MsgType: normalMsgType}, - Result: GetAuthorizationResult{Auth: nil, Exp: nil}, - }, + wasmAddrs: []sdk.AccAddress{addr5}, + existingOwners: []sdk.AccAddress{addr1, addr2, addr3, addr4}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr5}, + expErr: missingSig(addr3), + expIsMarkerCalls: []sdk.AccAddress{addr1, addr2, addr3}, + }, + { + name: "one existing is marker: first signer is wasm so the others are not returned", + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr5}, + {Granter: addr2, Grantee: addr5}, + {Granter: addr3, Grantee: addr5}, + {Granter: addr4, Grantee: addr5}, }, - }, - { - name: "addr to empty authz error", - existing: accStr("existing"), - proposed: "", - msg: normalMsg(noneAddr), - authKeeper: NewMockAuthKeeper(), - authzKeeper: NewMockAuthzKeeper().WithGetAuthorizationResults( - GetAuthorizationCall{ - GrantInfo: GrantInfo{Grantee: noneAddrAcc, Granter: acc("existing"), MsgType: normalMsgType}, - Result: GetAuthorizationResult{ - Auth: NewMockAuthorization("one", - authz.AcceptResponse{ - Accept: true, - Updated: NewMockAuthorization("two", authz.AcceptResponse{}, nil), - }, - nil), - Exp: nil, - }, - }, - ).WithSaveGrantResults( - SaveGrantCall{ - Grantee: noneAddrAcc, - Granter: acc("existing"), - Auth: NewMockAuthorization("two", authz.AcceptResponse{}, nil), - Exp: nil, - Result: errors.New("test error from SaveGrant"), - }, - ), - expErr: fmt.Sprintf("authz error with existing value owner %q: %s", accStr("existing"), "test error from SaveGrant"), - expGetAccount: []*GetAccountCall{ - NewGetAccountCall(acc("existing"), nil), - NewGetAccountCall(noneAddrAcc, nil), + wasmAddrs: []sdk.AccAddress{addr5}, + markerAddrs: []sdk.AccAddress{addr2}, + existingOwners: []sdk.AccAddress{addr1, addr2, addr3, addr4}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr5, addr1, addr2, addr3, addr4}, + expAddrs: []sdk.AccAddress{addr5}, + expUsedSigners: types.NewUsedSignersMap().Use(addr5.String()), + expIsMarkerCalls: []sdk.AccAddress{addr1, addr2, addr3, addr4}, + }, + { + name: "first signer is not wasm so all are returned", + wasmAddrs: []sdk.AccAddress{addr1, addr3}, // Shouldn't matter since the first signer is all that's checked. + markerAddrs: []sdk.AccAddress{addr2}, + existingOwners: []sdk.AccAddress{addr1, addr2, addr3, addr4}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr5, addr1, addr2, addr3, addr4}, + expAddrs: []sdk.AccAddress{addr5, addr1, addr2, addr3, addr4}, + expUsedSigners: types.NewUsedSignersMap().Use(addr1.String(), addr2.String(), addr3.String(), addr4.String()), + }, + { + name: "four existing, two are signers, other two are markers", + markerAddrs: []sdk.AccAddress{addr1, addr4}, + existingOwners: []sdk.AccAddress{addr1, addr2, addr3, addr4}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr2, addr3}, + expAddrs: []sdk.AccAddress{addr2, addr3}, + expUsedSigners: types.NewUsedSignersMap().Use(addr2.String(), addr3.String()), + expIsMarkerCalls: []sdk.AccAddress{addr1, addr4}, + }, + { + name: "two existing value owners: only first is signer", + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr1}, + expErr: missingSig(addr2), + expIsMarkerCalls: []sdk.AccAddress{addr2}, + }, + { + name: "two existing value owners: only second is signer", + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr2}, + expErr: missingSig(addr1), + expIsMarkerCalls: []sdk.AccAddress{addr1}, + }, + { + name: "two existing value owners: both are signers", + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr1, addr2}, + expAddrs: []sdk.AccAddress{addr1, addr2}, + expUsedSigners: types.NewUsedSignersMap().Use(addr1.String(), addr2.String()), + }, + { + name: "two existing value owners: both are signers in opposite order", + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr2, addr1}, + expAddrs: []sdk.AccAddress{addr2, addr1}, + expUsedSigners: types.NewUsedSignersMap().Use(addr1.String(), addr2.String()), + }, + { + name: "authz: two existing value owners to same other signer", + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr3}, + {Granter: addr2, Grantee: addr3}, }, - expGetAuth: []*GetAuthorizationCall{ - { - GrantInfo: GrantInfo{Grantee: noneAddrAcc, Granter: acc("existing"), MsgType: normalMsgType}, - Result: GetAuthorizationResult{ - Auth: NewMockAuthorization("one", - authz.AcceptResponse{ - Accept: true, - Updated: NewMockAuthorization("two", authz.AcceptResponse{}, nil), - }, - nil).WithAcceptCalls(normalMsg(noneAddr)), - Exp: nil, - }, - }, + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr3}, + expAddrs: []sdk.AccAddress{addr3}, + expUsedSigners: types.NewUsedSignersMap().Use(addr3.String()), + expIsMarkerCalls: []sdk.AccAddress{addr1, addr2}, + }, + { + name: "authz: two existing value owners only first gave grant", + authzGrants: []GrantInfo{{Granter: addr1, Grantee: addr3}}, + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr3}, + expErr: missingSig(addr2), + expIsMarkerCalls: []sdk.AccAddress{addr1, addr2}, + }, + { + name: "authz: two existing value owners only second gave grant", + authzGrants: []GrantInfo{{Granter: addr2, Grantee: addr3}}, + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr3}, + expErr: missingSig(addr1), + expIsMarkerCalls: []sdk.AccAddress{addr1}, + }, + { + name: "authz: two existing value owners to two different signers", + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr3}, + {Granter: addr2, Grantee: addr4}, }, + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr4, addr3}, + expAddrs: []sdk.AccAddress{addr4, addr3}, + expUsedSigners: types.NewUsedSignersMap().Use(addr3.String(), addr4.String()), + expIsMarkerCalls: []sdk.AccAddress{addr1, addr2}, }, - } - - for _, tc := range tests { - s.Run(tc.name, func() { - k := s.app.MetadataKeeper - origAuthKeeper := k.SetAuthKeeper(tc.authKeeper) - origAuthzKeeper := k.SetAuthzKeeper(tc.authzKeeper) - defer func() { - k.SetAuthKeeper(origAuthKeeper) - k.SetAuthzKeeper(origAuthzKeeper) - }() - if tc.expGetAccount != nil { - s.Require().NotNil(tc.authKeeper, "expGetAccount defined but test case does not have an authKeeper defined") - tc.authKeeper.GetAccountCalls = make([]*GetAccountCall, 0, len(tc.expGetAccount)) - } - if tc.expGetAuth != nil { - s.Require().NotNil(tc.authzKeeper, "expGetAuth defined but test case does not have an authzKeeper defined") - tc.authzKeeper.GetAuthorizationCalls = make([]*GetAuthorizationCall, 0, len(tc.expGetAuth)) - } - if tc.expUsed == nil && len(tc.expErr) == 0 { - tc.expUsed = keeper.NewUsedSignersMap() - } - - used, err := k.ValidateScopeValueOwnerUpdate(s.FreshCtx(), tc.existing, tc.proposed, tc.msg) - s.AssertErrorValue(err, tc.expErr, "ValidateScopeValueOwnerUpdate") - s.Assert().Equal(tc.expUsed, used, "ValidateScopeValueOwnerUpdate used signatures map") - - if tc.expGetAccount != nil { - getAccountCalls := tc.authKeeper.GetAccountCalls - s.Assert().Equal(tc.expGetAccount, getAccountCalls, "calls made to GetAccount") - } - if tc.expGetAuth != nil { - getAuthCalls := tc.authzKeeper.GetAuthorizationCalls - s.Assert().Equal(tc.expGetAuth, getAuthCalls, "calls made to GetAuthorization") - } - }) - } -} - -func (s *AuthzTestSuite) TestValidateScopeValueOwnerChangeFromExisting() { - markerDenom := "vochange" - markerAddr := markertypes.MustGetMarkerAddress(markerDenom) - addrWithWithdraw := sdk.AccAddress("addrWithWithdraw____") - addrAllButWithdraw := sdk.AccAddress("addrAllButWithdraw__") - addrOther := sdk.AccAddress("addrOther___________") - addrRand1 := sdk.AccAddress("addrRand1___________") - addrRand2 := sdk.AccAddress("addrRand1___________") - addrWithWithdrawStr := addrWithWithdraw.String() - addrAllButWithdrawStr := addrAllButWithdraw.String() - addrOtherStr := addrOther.String() - addrRand1Str := addrRand1.String() - addrRand2Str := addrRand2.String() - - marker := &markertypes.MarkerAccount{ - BaseAccount: authtypes.NewBaseAccount(markerAddr, nil, 0, 0), - Status: markertypes.StatusActive, - Denom: markerDenom, - Supply: sdkmath.NewInt(1000), - MarkerType: markertypes.MarkerType_RestrictedCoin, - AccessControl: []markertypes.AccessGrant{ - { - Address: addrWithWithdrawStr, - Permissions: markertypes.AccessList{markertypes.Access_Withdraw}}, - { - Address: addrAllButWithdrawStr, - Permissions: markertypes.AccessList{ - markertypes.Access_Mint, markertypes.Access_Burn, - markertypes.Access_Deposit, markertypes.Access_Delete, - markertypes.Access_Admin, markertypes.Access_Transfer, - }, - }, - }, - } - - sw := func(addrs ...string) *keeper.SignersWrapper { - return keeper.NewSignersWrapper(addrs) - } - used := func(addrs ...string) keeper.UsedSignersMap { - return keeper.NewUsedSignersMap().Use(addrs...) - } - - msg := &types.MsgWriteScopeRequest{} - msgType := types.TypeURLMsgWriteScopeRequest - - missingSig := func(addr string) string { - return "missing signature from existing value owner " + addr - } - - tests := []struct { - name string - existing string - signers *keeper.SignersWrapper - authKeeper *MockAuthKeeper - authzKeeper *MockAuthzKeeper - expErr string - expUsed keeper.UsedSignersMap - expAuthzCalls []*GetAuthorizationCall - }{ { - name: "no existing", - existing: "", - expErr: "", - }, - { - name: "is marker does not have withdraw", - existing: markerAddr.String(), - signers: sw(addrAllButWithdrawStr), - authKeeper: NewMockAuthKeeper().WithGetAccountResults(NewGetAccountCall(markerAddr, marker)), - expErr: fmt.Sprintf("missing signature for %s (%s) with authority to withdraw/remove it as scope value owner", markerAddr.String(), markerDenom), - }, - { - name: "is marker has withdraw", - existing: markerAddr.String(), - signers: sw(addrWithWithdrawStr), - authKeeper: NewMockAuthKeeper().WithGetAccountResults(NewGetAccountCall(markerAddr, marker)), - expUsed: used(addrWithWithdrawStr), - }, - { - name: "is only signer", - existing: addrOtherStr, - signers: sw(addrOtherStr), - expErr: "", - expUsed: used(addrOtherStr), - }, - { - name: "is 1st of 3 signers", - existing: addrOtherStr, - signers: sw(addrOtherStr, addrRand1Str, addrRand2Str), - expUsed: used(addrOtherStr), + name: "authz: two existing value owners first is signer and second gave grant to first", + authzGrants: []GrantInfo{ + {Granter: addr2, Grantee: addr1}, + }, + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr1}, + expAddrs: []sdk.AccAddress{addr1}, + expUsedSigners: types.NewUsedSignersMap().Use(addr1.String()), + expIsMarkerCalls: []sdk.AccAddress{addr2}, }, { - name: "is 2nd of 3 signers", - existing: addrOtherStr, - signers: sw(addrRand1Str, addrOtherStr, addrRand2Str), - expUsed: used(addrOtherStr), + name: "authz: two existing value owners second is signer and first gave grant to second", + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr2}, + }, + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr2}, + expAddrs: []sdk.AccAddress{addr2}, + expUsedSigners: types.NewUsedSignersMap().Use(addr2.String()), + expIsMarkerCalls: []sdk.AccAddress{addr1}, }, { - name: "is 3rd of 3 signers", - existing: addrOtherStr, - signers: sw(addrRand1Str, addrRand2Str, addrOtherStr), - expUsed: used(addrOtherStr), + name: "authz: error from authorization followup", + authzErrs: []GrantInfo{ + {Granter: addr2, Grantee: addr3}, + }, + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr3}, + }, + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr3}, + expErr: authzErr(addr2), + expIsMarkerCalls: []sdk.AccAddress{addr1, addr2}, }, { - name: "isn't signer isn't bech32", - existing: "not-a-bech32", - signers: sw(addrOtherStr), - expErr: missingSig("not-a-bech32"), + name: "only authz grant is for migrate", + msgMakers: []msgMaker{msgMakerUpdate, msgMakerWriteScope, msgMakerDeleteScope}, + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr3, MsgType: msgMakerMigrate.msgType}, + }, + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr3}, + expErr: missingSig(addr1), + expIsMarkerCalls: []sdk.AccAddress{addr1}, }, { - name: "isn't signer but authed a signer", - existing: addrOtherStr, - signers: sw(addrRand1Str), - authzKeeper: NewMockAuthzKeeper().WithGetAuthorizationResults( - NewAcceptedGetAuthorizationCall(addrRand1, addrOther, msgType, "one"), - ), - expUsed: used(addrRand1Str), - expAuthzCalls: []*GetAuthorizationCall{ - NewAcceptedGetAuthorizationCall(addrRand1, addrOther, msgType, "one").WithAcceptCalls(msg), + name: "only authz grant is for update", + msgMakers: []msgMaker{msgMakerMigrate, msgMakerWriteScope, msgMakerDeleteScope}, + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr3, MsgType: msgMakerUpdate.msgType}, }, + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr3}, + expErr: missingSig(addr1), + expIsMarkerCalls: []sdk.AccAddress{addr1}, }, { - name: "isn't signer but authed smart contract signer", - existing: addrOtherStr, - signers: sw(addrRand2Str), - authKeeper: NewMockAuthKeeper().WithGetAccountResults(NewWasmGetAccountCall(addrRand2)), - authzKeeper: NewMockAuthzKeeper().WithGetAuthorizationResults( - NewAcceptedGetAuthorizationCall(addrRand2, addrOther, msgType, "one"), - ), - expErr: missingSig(addrOtherStr), - expAuthzCalls: nil, // should not have checked authz. + name: "only authz grant is for write scope", + msgMakers: []msgMaker{msgMakerUpdate, msgMakerMigrate, msgMakerDeleteScope}, + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr3, MsgType: msgMakerWriteScope.msgType}, + }, + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr3}, + expErr: missingSig(addr1), + expIsMarkerCalls: []sdk.AccAddress{addr1}, }, { - name: "isn't signer no auth", - existing: addrOtherStr, - signers: sw(addrRand1Str, addrRand2Str), - expErr: missingSig(addrOtherStr), - expAuthzCalls: []*GetAuthorizationCall{ - NewNotFoundGetAuthorizationCall(addrRand1, addrOther, msgType), - NewNotFoundGetAuthorizationCall(addrRand2, addrOther, msgType), + name: "only authz grant is for delete scope", + msgMakers: []msgMaker{msgMakerUpdate, msgMakerMigrate, msgMakerWriteScope}, + authzGrants: []GrantInfo{ + {Granter: addr1, Grantee: addr3, MsgType: msgMakerDeleteScope.msgType}, }, + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr3}, + expErr: missingSig(addr1), + expIsMarkerCalls: []sdk.AccAddress{addr1}, }, { - name: "error from authorization", - existing: addrOtherStr, - signers: sw(addrRand1Str), - authzKeeper: NewMockAuthzKeeper().WithGetAuthorizationResults( - GetAuthorizationCall{ - GrantInfo: GrantInfo{ - Grantee: addrRand1, - Granter: addrOther, - MsgType: msgType, - }, - Result: GetAuthorizationResult{ - Auth: NewMockAuthorization("one", authz.AcceptResponse{Accept: true, Delete: true}, nil), - Exp: nil, + name: "invalid first signer", + msgMakers: []msgMaker{ + { + name: msgMakerUpdate.name, + msgType: msgMakerUpdate.msgType, + make: func(_ []sdk.AccAddress) types.MetadataMsg { + return &types.MsgUpdateValueOwnersRequest{Signers: []string{"nope"}} }, }, - ).WithDeleteGrantResults(DeleteGrantCall{ - GrantInfo: GrantInfo{ - Grantee: addrRand1, - Granter: addrOther, - MsgType: msgType, + { + name: msgMakerMigrate.name, + msgType: msgMakerMigrate.msgType, + make: func(_ []sdk.AccAddress) types.MetadataMsg { + return &types.MsgMigrateValueOwnerRequest{Signers: []string{"nope"}} + }, }, - Result: fmt.Errorf("test error upon delete"), - }), - expErr: "authz error with existing value owner \"" + addrOtherStr + "\": test error upon delete", - expAuthzCalls: []*GetAuthorizationCall{ { - GrantInfo: GrantInfo{ - Grantee: addrRand1, - Granter: addrOther, - MsgType: msgType, + name: msgMakerWriteScope.name, + msgType: msgMakerWriteScope.msgType, + make: func(_ []sdk.AccAddress) types.MetadataMsg { + return &types.MsgWriteScopeRequest{Signers: []string{"nope"}} }, - Result: GetAuthorizationResult{ - Auth: NewMockAuthorization("one", authz.AcceptResponse{Accept: true, Delete: true}, nil).WithAcceptCalls(msg), - Exp: nil, + }, + { + name: msgMakerDeleteScope.name, + msgType: msgMakerDeleteScope.msgType, + make: func(_ []sdk.AccAddress) types.MetadataMsg { + return &types.MsgDeleteScopeRequest{Signers: []string{"nope"}} }, }, }, + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr6.String(), + expErr: "invalid signer address \"nope\": decoding bech32 failed: invalid bech32 string length 4", }, - } - - for _, tc := range tests { - s.Run(tc.name, func() { - if tc.authKeeper == nil { - tc.authKeeper = NewMockAuthKeeper() - } - if tc.authzKeeper == nil { - tc.authzKeeper = NewMockAuthzKeeper() - } - if tc.expUsed == nil && len(tc.expErr) == 0 { - tc.expUsed = keeper.NewUsedSignersMap() - } - mdKeeper := s.app.MetadataKeeper - origAuthKeeper := mdKeeper.SetAuthKeeper(tc.authKeeper) - origAuthzKeeper := mdKeeper.SetAuthzKeeper(tc.authzKeeper) - defer func() { - mdKeeper.SetAuthKeeper(origAuthKeeper) - mdKeeper.SetAuthzKeeper(origAuthzKeeper) - }() - - usedSigners, err := mdKeeper.ValidateScopeValueOwnerChangeFromExisting(s.FreshCtx(), tc.existing, tc.signers, msg) - s.AssertErrorValue(err, tc.expErr, "validateScopeValueOwnerChangeFromExisting error") - s.Assert().Equal(tc.expUsed, usedSigners, "validateScopeValueOwnerChangeFromExisting used address map") - - authCalls := tc.authzKeeper.GetAuthorizationCalls - s.Assert().Equal(tc.expAuthzCalls, authCalls) - }) - } -} - -func (s *AuthzTestSuite) TestValidateScopeValueOwnerChangeToProposed() { - markerDenom := "vochange" - markerAddr := markertypes.MustGetMarkerAddress(markerDenom) - addrWithDeposit := sdk.AccAddress("addrWithDeposit_____") - addrAllButDeposit := sdk.AccAddress("addrAllButDeposit___") - addrOther := sdk.AccAddress("addrOther___________") - addrWithDepositStr := addrWithDeposit.String() - addrAllButDepositStr := addrAllButDeposit.String() - addrOtherStr := addrOther.String() - - marker := &markertypes.MarkerAccount{ - BaseAccount: authtypes.NewBaseAccount(markerAddr, nil, 0, 0), - Status: markertypes.StatusActive, - Denom: markerDenom, - Supply: sdkmath.NewInt(1000), - MarkerType: markertypes.MarkerType_RestrictedCoin, - AccessControl: []markertypes.AccessGrant{ - { - Address: addrWithDepositStr, - Permissions: markertypes.AccessList{markertypes.Access_Deposit}}, - { - Address: addrAllButDepositStr, - Permissions: markertypes.AccessList{ - markertypes.Access_Mint, markertypes.Access_Burn, - markertypes.Access_Withdraw, markertypes.Access_Delete, - markertypes.Access_Admin, markertypes.Access_Transfer, + { + name: "invalid second signer", + msgMakers: []msgMaker{ + { + name: msgMakerUpdate.name, + msgType: msgMakerUpdate.msgType, + make: func(_ []sdk.AccAddress) types.MetadataMsg { + return &types.MsgUpdateValueOwnersRequest{Signers: []string{addr1.String(), "nope"}} + }, + }, + { + name: msgMakerMigrate.name, + msgType: msgMakerMigrate.msgType, + make: func(_ []sdk.AccAddress) types.MetadataMsg { + return &types.MsgMigrateValueOwnerRequest{Signers: []string{addr1.String(), "nope"}} + }, + }, + { + name: msgMakerWriteScope.name, + msgType: msgMakerWriteScope.msgType, + make: func(_ []sdk.AccAddress) types.MetadataMsg { + return &types.MsgWriteScopeRequest{Signers: []string{addr1.String(), "nope"}} + }, + }, + { + name: msgMakerDeleteScope.name, + msgType: msgMakerDeleteScope.msgType, + make: func(_ []sdk.AccAddress) types.MetadataMsg { + return &types.MsgDeleteScopeRequest{Signers: []string{addr1.String(), "nope"}} + }, }, }, - }, - } - - sw := func(addrs ...string) *keeper.SignersWrapper { - return keeper.NewSignersWrapper(addrs) - } - used := func(addrs ...string) keeper.UsedSignersMap { - return keeper.NewUsedSignersMap().Use(addrs...) - } - - tests := []struct { - name string - proposed string - signers *keeper.SignersWrapper - expErr string - expUsed keeper.UsedSignersMap - }{ - { - name: "no proposed", - proposed: "", - signers: sw(addrOtherStr), - expErr: "", - expUsed: used(), - }, - { - name: "is marker does not have deposit", - proposed: markerAddr.String(), - signers: sw(addrAllButDepositStr), - expErr: fmt.Sprintf("missing signature for %s (%s) with authority to deposit/add it as scope value owner", markerAddr.String(), markerDenom), - expUsed: nil, + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr6.String(), + expErr: "invalid signer[1] address \"nope\": decoding bech32 failed: invalid bech32 string length 4", }, { - name: "is marker has deposit", - proposed: markerAddr.String(), - signers: sw(addrWithDepositStr), - expErr: "", - expUsed: used(addrWithDepositStr), + name: "authz grant on marker account", + authzErrs: []GrantInfo{ + // Set the grant up to fail because it shouldn't actually be involved. + {Granter: addr1, Grantee: addr3}, + }, + markerAddrs: []sdk.AccAddress{addr1}, + existingOwners: []sdk.AccAddress{addr1}, + proposed: addr6.String(), + signers: []sdk.AccAddress{addr3}, + expAddrs: []sdk.AccAddress{addr3}, + expUsedSigners: types.NewUsedSignersMap(), + expIsMarkerCalls: []sdk.AccAddress{addr1}, }, { - name: "is not marker", - proposed: sdk.AccAddress("some_other_address__").String(), - signers: sw(addrOtherStr), - expErr: "", - expUsed: used(), + name: "three existing: signer, marker, proposed", + markerAddrs: []sdk.AccAddress{addr2}, + existingOwners: []sdk.AccAddress{addr1, addr2, addr3}, + proposed: addr3.String(), + signers: []sdk.AccAddress{addr1}, + expAddrs: []sdk.AccAddress{addr1}, + expUsedSigners: types.NewUsedSignersMap().Use(addr1.String()), + expIsMarkerCalls: []sdk.AccAddress{addr2}, }, { - name: "is not bech32", - proposed: "not a bech32 address string", - signers: sw(addrOtherStr), - expErr: "", - expUsed: used(), + name: "two existing: first is proposed, second is NOT signer", + existingOwners: []sdk.AccAddress{addr1, addr2}, + proposed: addr1.String(), + signers: []sdk.AccAddress{addr3}, + expErr: missingSig(addr2), + expIsMarkerCalls: []sdk.AccAddress{addr2}, }, } - for _, tc := range tests { - s.Run(tc.name, func() { - authKeeper := NewMockAuthKeeper().WithGetAccountResults(NewGetAccountCall(markerAddr, marker)) - mdKeeper := s.app.MetadataKeeper - origAuthKeeper := mdKeeper.SetAuthKeeper(authKeeper) - defer mdKeeper.SetAuthKeeper(origAuthKeeper) - - usedSigners, err := mdKeeper.ValidateScopeValueOwnerChangeToProposed(s.FreshCtx(), tc.proposed, tc.signers) - s.AssertErrorValue(err, tc.expErr, "validateScopeValueOwnerChangeToProposed error") - s.Assert().Equal(tc.expUsed, usedSigners, "validateScopeValueOwnerChangeToProposed used signers") - }) + for i, tc := range tests { + if len(tc.msgMakers) == 0 { + tc.msgMakers = allMsgMakers + } + for m, maker := range tc.msgMakers { + s.Run(fmt.Sprintf("%d %s: %s", i, maker.name, tc.name), func() { + kpr := s.app.MetadataKeeper + + authzK := NewMockAuthzKeeper() + if len(tc.authzGrants) > 0 { + entries := make([]GetAuthorizationCall, len(tc.authzGrants)) + for g, grant := range tc.authzGrants { + msgType := grant.MsgType + if len(msgType) == 0 { + msgType = maker.msgType + } + authName := fmt.Sprintf("good_%d_%d_%d", i, m, g) + entries[g] = NewAcceptedGetAuthorizationCall(grant.Grantee, grant.Granter, msgType, authName) + } + authzK = authzK.WithGetAuthorizationResults(entries...) + } + if len(tc.authzErrs) > 0 { + getEntries := make([]GetAuthorizationCall, len(tc.authzErrs)) + delEntries := make([]DeleteGrantCall, len(tc.authzErrs)) + for g, grant := range tc.authzErrs { + msgType := grant.MsgType + if len(msgType) == 0 { + msgType = maker.msgType + } + authName := fmt.Sprintf("bad_%d_%d_%d", i, m, g) + getEntries[g] = GetAuthorizationCall{ + GrantInfo: GrantInfo{Granter: grant.Granter, Grantee: grant.Grantee, MsgType: msgType}, + Result: GetAuthorizationResult{ + Auth: NewMockAuthorization(authName, authz.AcceptResponse{Accept: true, Delete: true}, nil), + }, + } + delEntries[g] = DeleteGrantCall{ + GrantInfo: GrantInfo{Granter: grant.Granter, Grantee: grant.Grantee, MsgType: msgType}, + Result: errors.New(injectedErrorStr), + } + } + authzK = authzK.WithGetAuthorizationResults(getEntries...) + authzK = authzK.WithDeleteGrantResults(delEntries...) + } + defer kpr.SetAuthzKeeper(kpr.SetAuthzKeeper(authzK)) + + markerK := NewMockMarkerKeeper() + if len(tc.markerAddrs) > 0 { + markerK = markerK.WithIsMarkerAccountResults(tc.markerAddrs...) + } + defer kpr.SetMarkerKeeper(kpr.SetMarkerKeeper(markerK)) + + msg := maker.make(tc.signers) + + ctx := s.FreshCtx() + if len(tc.wasmAddrs) > 0 { + cache := types.GetAuthzCache(ctx) + for _, addr := range tc.wasmAddrs { + cache.SetIsWasm(addr, true) + } + } + + var actAddrs []sdk.AccAddress + var actUsedAddrs types.UsedSignersMap + var actErr error + testFunc := func() { + actAddrs, actUsedAddrs, actErr = kpr.ValidateScopeValueOwnersSigners(ctx, tc.existingOwners, tc.proposed, msg) + } + s.Require().NotPanics(testFunc, "ValidateScopeValueOwnersSigners") + s.AssertErrorValue(actErr, tc.expErr, "error from ValidateScopeValueOwnersSigners") + s.Assert().Equal(tc.expAddrs, actAddrs, "addresses from ValidateScopeValueOwnersSigners") + s.Assert().Equal(tc.expUsedSigners, actUsedAddrs, "UsedSignersMap from ValidateScopeValueOwnersSigners") + markerK.AssertIsMarkerAccountCalls(s.T(), tc.expIsMarkerCalls) + }) + } } } @@ -6094,8 +5712,8 @@ func (s *AuthzTestSuite) TestValidateAllRequiredSigned() { randAddr3 := sdk.AccAddress("random_address_3____").String() // expFoundSigner creates a PartyDetails for a party found as a signer. - expFoundSigner := func(addr string) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ + expFoundSigner := func(addr string) *types.PartyDetails { + return types.TestablePartyDetails{ Address: addr, Role: types.PartyType_PARTY_TYPE_UNSPECIFIED, Optional: false, @@ -6107,8 +5725,8 @@ func (s *AuthzTestSuite) TestValidateAllRequiredSigned() { }.Real() } // expFoundAuthz creates a PartyDetails for a party found via authz. - expFoundAuthz := func(addr string, signer sdk.AccAddress) *keeper.PartyDetails { - rv := keeper.TestablePartyDetails{ + expFoundAuthz := func(addr string, signer sdk.AccAddress) *types.PartyDetails { + rv := types.TestablePartyDetails{ Address: addr, Role: types.PartyType_PARTY_TYPE_UNSPECIFIED, Optional: false, @@ -6122,7 +5740,7 @@ func (s *AuthzTestSuite) TestValidateAllRequiredSigned() { return rv } // pdz is just a shorter way of creating a slice of PartyDetails. - pdz := func(details ...*keeper.PartyDetails) []*keeper.PartyDetails { + pdz := func(details ...*types.PartyDetails) []*types.PartyDetails { return details } @@ -6130,7 +5748,7 @@ func (s *AuthzTestSuite) TestValidateAllRequiredSigned() { name string owners []string msg types.MetadataMsg - exp []*keeper.PartyDetails + exp []*types.PartyDetails errorMsg string }{ { @@ -6279,7 +5897,7 @@ func (s *AuthzTestSuite) TestValidateAllRequiredSigned() { for _, tc := range tests { s.T().Run(tc.name, func(t *testing.T) { actual, err := s.app.MetadataKeeper.ValidateAllRequiredSigned(s.FreshCtx(), tc.owners, tc.msg) - AssertErrorValue(t, err, tc.errorMsg, "ValidateSignersWithoutParties unexpected error") + s.AssertErrorValue(err, tc.errorMsg, "ValidateSignersWithoutParties unexpected error") assert.Equal(t, tc.exp, actual, "ValidateSignersWithoutParties validated parties") }) } @@ -6566,7 +6184,7 @@ func TestValidateRolesPresent(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err := keeper.ValidateRolesPresent(tc.parties, tc.reqRoles) - AssertErrorValue(t, err, tc.exp, "validateRolesPresent") + assertions.AssertErrorValue(t, err, tc.exp, "validateRolesPresent") }) } } @@ -6667,175 +6285,7 @@ func TestValidatePartiesArePresent(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err := keeper.ValidatePartiesArePresent(tc.required, tc.available) - AssertErrorValue(t, err, tc.exp, "validatePartiesArePresent") - }) - } -} - -func (s *AuthzTestSuite) TestGetMarkerAndCheckAuthority() { - markerAddr := markertypes.MustGetMarkerAddress("testcoin").String() - marker := markertypes.MarkerAccount{ - BaseAccount: &authtypes.BaseAccount{ - Address: markerAddr, - AccountNumber: 23, - }, - AccessControl: []markertypes.AccessGrant{ - { - Address: s.user1, - Permissions: markertypes.AccessListByNames("deposit,withdraw"), - }, - { - Address: s.user2, - Permissions: markertypes.AccessListByNames("burn,mint"), - }, - }, - Denom: "testcoin", - Supply: sdkmath.NewInt(1000), - MarkerType: markertypes.MarkerType_Coin, - Status: markertypes.StatusActive, - } - s.Require().NoError(s.app.MarkerKeeper.AddMarkerAccount(s.FreshCtx(), &marker), "AddMarkerAccount") - // s.user1 has an account created in TestSetup. - - tests := []struct { - name string - addr string - signers []string - role markertypes.Access - expMarker markertypes.MarkerAccountI - expHasAcc bool - expSig string - }{ - {name: "invalid address", addr: "invalid", expMarker: nil}, - {name: "account does not exist", addr: sdk.AccAddress("does-not-exist").String(), expMarker: nil}, - {name: "account exists but is not marker", addr: s.user1, expMarker: nil}, - { - name: "is marker does not have signer", - addr: markerAddr, - signers: []string{s.user3}, - expMarker: &marker, - expHasAcc: false, - }, - { - name: "is marker with signer 1 but not role", - addr: markerAddr, - signers: []string{s.user1}, - role: markertypes.Access_Transfer, - expMarker: &marker, - expHasAcc: false, - }, - { - name: "is marker with signer 1 with role other user has", - addr: markerAddr, - signers: []string{s.user1}, - role: markertypes.Access_Burn, - expMarker: &marker, - expHasAcc: false, - }, - { - name: "is marker with signer 1 and role 1", - addr: markerAddr, - signers: []string{s.user1}, - role: markertypes.Access_Deposit, - expMarker: &marker, - expHasAcc: true, - expSig: s.user1, - }, - { - name: "is marker with signer 1 and role 2", - addr: markerAddr, - signers: []string{s.user1}, - role: markertypes.Access_Withdraw, - expMarker: &marker, - expHasAcc: true, - expSig: s.user1, - }, - { - name: "is marker with signer 2 but not role", - addr: markerAddr, - signers: []string{s.user2}, - role: markertypes.Access_Transfer, - expMarker: &marker, - expHasAcc: false, - }, - { - name: "is marker with signer 2 with role other user has", - addr: markerAddr, - signers: []string{s.user2}, - role: markertypes.Access_Deposit, - expMarker: &marker, - expHasAcc: false, - }, - { - name: "is marker with signer 2 and role 1", - addr: markerAddr, - signers: []string{s.user2}, - role: markertypes.Access_Burn, - expMarker: &marker, - expHasAcc: true, - expSig: s.user2, - }, - { - name: "is marker with signer 2 and role 2", - addr: markerAddr, - signers: []string{s.user2}, - role: markertypes.Access_Mint, - expMarker: &marker, - expHasAcc: true, - expSig: s.user2, - }, - { - name: "is marker both signers role from first", - addr: markerAddr, - signers: []string{s.user1, s.user2}, - role: markertypes.Access_Withdraw, - expMarker: &marker, - expHasAcc: true, - expSig: s.user1, - }, - { - name: "is marker both signers role from second", - addr: markerAddr, - signers: []string{s.user1, s.user2}, - role: markertypes.Access_Mint, - expMarker: &marker, - expHasAcc: true, - expSig: s.user2, - }, - { - name: "is marker both signers neither have role", - addr: markerAddr, - signers: []string{s.user1, s.user2}, - role: markertypes.Access_Transfer, - expMarker: &marker, - expHasAcc: false, - }, - { - name: "is marker two signers first has role", - addr: markerAddr, - signers: []string{s.user1, s.user3}, - role: markertypes.Access_Withdraw, - expMarker: &marker, - expHasAcc: true, - expSig: s.user1, - }, - { - name: "is marker two signers second has role", - addr: markerAddr, - signers: []string{s.user3, s.user2}, - role: markertypes.Access_Burn, - expMarker: &marker, - expHasAcc: true, - expSig: s.user2, - }, - } - - for _, tc := range tests { - s.Run(tc.name, func() { - actualMarker, actualHasAcc, actualSig := s.app.MetadataKeeper.GetMarkerAndCheckAuthority(s.FreshCtx(), tc.addr, tc.signers, tc.role) - s.Assert().Equal(tc.expMarker, actualMarker, "GetMarkerAndCheckAuthority marker") - s.Assert().Equal(tc.expHasAcc, actualHasAcc, "GetMarkerAndCheckAuthority has access") - s.Assert().Equal(tc.expSig, actualSig, "GetMarkerAndCheckAuthority signer") + assertions.AssertErrorValue(t, err, tc.exp, "validatePartiesArePresent") }) } } diff --git a/x/metadata/keeper/signers_utils.go b/x/metadata/keeper/signers_utils.go index 483a3898f6..40c1a41f50 100644 --- a/x/metadata/keeper/signers_utils.go +++ b/x/metadata/keeper/signers_utils.go @@ -1,203 +1,13 @@ package keeper import ( - "bytes" "context" - "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/authz" "github.com/provenance-io/provenance/x/metadata/types" ) -// PartyDetails is a struct used to help process party and signer validation. -// Even though all the fields are public, you should usually use the Get/Set methods -// which handle automatic bech32 conversion when needed and reduce duplicated efforts. -type PartyDetails struct { - address string - role types.PartyType - optional bool - - acc sdk.AccAddress - signer string - signerAcc sdk.AccAddress - - canBeUsedBySpec bool - usedBySpec bool -} - -// WrapRequiredParty creates a PartyDetails from the provided Party. -func WrapRequiredParty(party types.Party) *PartyDetails { - return &PartyDetails{ - address: party.Address, - role: party.Role, - optional: party.Optional, - } -} - -// WrapAvailableParty creates a PartyDetails from the provided Party and marks it as optional and usable. -func WrapAvailableParty(party types.Party) *PartyDetails { - return &PartyDetails{ - address: party.Address, - role: party.Role, - optional: true, // An available party is optional unless something else says otherwise. - canBeUsedBySpec: true, - } -} - -// BuildPartyDetails creates the list of PartyDetails to be used in party/signer/role validation. -func BuildPartyDetails(reqParties, availableParties []types.Party) []*PartyDetails { - details := make([]*PartyDetails, 0, len(availableParties)) - - // Start with creating details for each available party. -availablePartiesLoop: - for _, party := range availableParties { - for _, known := range details { - if party.IsSameAs(known) { - continue availablePartiesLoop - } - } - details = append(details, WrapAvailableParty(party)) - } - - // Now update the details to include optional=false required parties. - // If an equal party is already in the details, just update its optional flag - // to false, otherwise, add it to the list. -reqPartiesLoop: - for _, reqParty := range reqParties { - if !reqParty.Optional { - for _, party := range details { - if reqParty.IsSameAs(party) { - party.MakeRequired() - continue reqPartiesLoop - } - } - details = append(details, WrapRequiredParty(reqParty)) - } - } - - return details -} - -func (p *PartyDetails) SetAddress(address string) { - if p.address != address { - p.acc = nil - } - p.address = address -} - -func (p *PartyDetails) GetAddress() string { - if len(p.address) == 0 && len(p.acc) > 0 { - p.address = p.acc.String() - } - return p.address -} - -func (p *PartyDetails) SetAcc(addr sdk.AccAddress) { - if !bytes.Equal(p.acc, addr) { - p.address = "" - } - p.acc = addr -} - -func (p *PartyDetails) GetAcc() sdk.AccAddress { - if len(p.acc) == 0 && len(p.address) > 0 { - p.acc, _ = sdk.AccAddressFromBech32(p.address) - } - return p.acc -} - -func (p *PartyDetails) SetRole(role types.PartyType) { - p.role = role -} - -func (p *PartyDetails) GetRole() types.PartyType { - return p.role -} - -func (p *PartyDetails) SetOptional(optional bool) { - p.optional = optional -} - -func (p *PartyDetails) MakeRequired() { - p.optional = false -} - -func (p *PartyDetails) GetOptional() bool { - return p.optional -} - -func (p *PartyDetails) IsRequired() bool { - return !p.optional -} - -func (p *PartyDetails) SetSigner(signer string) { - if p.signer != signer { - p.signerAcc = nil - } - p.signer = signer -} - -func (p *PartyDetails) GetSigner() string { - if len(p.signer) == 0 && len(p.signerAcc) > 0 { - p.signer = p.signerAcc.String() - } - return p.signer -} - -func (p *PartyDetails) SetSignerAcc(signerAddr sdk.AccAddress) { - if !bytes.Equal(p.signerAcc, signerAddr) { - p.signer = "" - } - p.signerAcc = signerAddr -} - -func (p *PartyDetails) GetSignerAcc() sdk.AccAddress { - if len(p.signerAcc) == 0 && len(p.signer) > 0 { - p.signerAcc, _ = sdk.AccAddressFromBech32(p.signer) - } - return p.signerAcc -} - -func (p *PartyDetails) HasSigner() bool { - return len(p.signer) > 0 || len(p.signerAcc) > 0 -} - -func (p *PartyDetails) CanBeUsed() bool { - return p.canBeUsedBySpec -} - -func (p *PartyDetails) MarkAsUsed() { - p.usedBySpec = true -} - -func (p *PartyDetails) IsUsed() bool { - return p.usedBySpec -} - -// IsStillUsableAs returns true if this party can be used, hasn't yet been used and has the provided role. -func (p *PartyDetails) IsStillUsableAs(role types.PartyType) bool { - return p.CanBeUsed() && !p.IsUsed() && p.GetRole() == role -} - -// IsSameAs returns true if this is the same as the provided Party or PartyDetails. -// Only the address and role are considered for this test. -func (p *PartyDetails) IsSameAs(p2 types.Partier) bool { - return types.SamePartiers(p, p2) -} - -// GetUsedSigners gets a map of bech32 strings to true with a key for each used signer. -func GetUsedSigners(parties []*PartyDetails) UsedSignersMap { - rv := make(UsedSignersMap) - for _, party := range parties { - if party.HasSigner() { - rv.Use(party.GetSigner()) - } - } - return rv -} - // SignersWrapper stores the signers as strings and acc addresses. // One is created by providing the strings. They are then converted to acc addresses // if they're needed that way. @@ -227,185 +37,14 @@ func (s *SignersWrapper) Accs() []sdk.AccAddress { return s.signerAccs } -// authzCacheAcceptableKey creates the key string used in the AuthzCache.acceptable map. -func authzCacheAcceptableKey(grantee, granter sdk.AccAddress, msgTypeURL string) string { - return string(grantee) + "-" + string(granter) + "-" + msgTypeURL -} - -// authzCacheIsWasmKey creates the key string used in the AuthzCache.known map. -func authzCacheIsWasmKey(addr sdk.AccAddress) string { - return string(addr) -} - -// AuthzCache is a struct that houses a map of authz authorizations that are known to have a passed Accept (and been handled). -type AuthzCache struct { - acceptable map[string]authz.Authorization - isWasm map[string]bool -} - -// NewAuthzCache creates a new AuthzCache. -func NewAuthzCache() *AuthzCache { - return &AuthzCache{ - acceptable: make(map[string]authz.Authorization), - isWasm: make(map[string]bool), - } -} - -// Clear deletes all entries from this AuthzCache. -func (c *AuthzCache) Clear() { - for k := range c.acceptable { - delete(c.acceptable, k) - } - for k := range c.isWasm { - delete(c.isWasm, k) - } -} - -// SetAcceptable sets an authorization in this cache as acceptable. -func (c *AuthzCache) SetAcceptable(grantee, granter sdk.AccAddress, msgTypeURL string, authorization authz.Authorization) { - c.acceptable[authzCacheAcceptableKey(grantee, granter, msgTypeURL)] = authorization -} - -// GetAcceptable gets a previously set acceptable authorization. -// Returns nil if no such authorization exists. -func (c *AuthzCache) GetAcceptable(grantee, granter sdk.AccAddress, msgTypeURL string) authz.Authorization { - return c.acceptable[authzCacheAcceptableKey(grantee, granter, msgTypeURL)] -} - -// SetIsWasm records whether an account is a wasm account. -func (c *AuthzCache) SetIsWasm(addr sdk.AccAddress, value bool) { - c.isWasm[authzCacheIsWasmKey(addr)] = value -} - -// HasIsWasm returns true if a cached IsWasm value has been recorded for the given address. -// Use GetIsWasm to get the previously recorded IsWasm value. -func (c *AuthzCache) HasIsWasm(addr sdk.AccAddress) bool { - _, rv := c.isWasm[authzCacheIsWasmKey(addr)] - return rv -} - -// GetIsWasm returns true if the address was previously recorded as being a wasm account. -// Returns false if either: -// - The address was previously recorded as NOT being a wasm account. -// - The WASM status of the account hasn't yet been recorded. -// -// Use HasIsWasm to differentiate the false conditions. -func (c *AuthzCache) GetIsWasm(addr sdk.AccAddress) bool { - return c.isWasm[authzCacheIsWasmKey(addr)] -} - -// authzCacheContextKey is the key used in an sdk.Context to set/get the AuthzCache. -const authzCacheContextKey = "authzCacheContextKey" - -// AddAuthzCacheToContext either returns a new sdk.Context with the addition of an AuthzCache, -// or clears out the AuthzCache if it already exists in the context. -// It panics if the AuthzCache key exists in the context but isn't an AuthzCache. -func AddAuthzCacheToContext(ctx sdk.Context) sdk.Context { - // If it's already got one, leave it there but clear it out. - // Otherwise, we'll add a new one. - if cacheV := ctx.Value(authzCacheContextKey); cacheV != nil { - if cache, ok := cacheV.(*AuthzCache); ok { - cache.Clear() - return ctx - } - // If the key was there, but not an AuthzCache, things are very wrong. Panic. - panic(fmt.Errorf("context value %q is a %T, expected %T", - authzCacheContextKey, cacheV, NewAuthzCache())) - } - return ctx.WithValue(authzCacheContextKey, NewAuthzCache()) -} - -// GetAuthzCache gets the AuthzCache from the context or panics. -func GetAuthzCache(ctx sdk.Context) *AuthzCache { - cacheV := ctx.Value(authzCacheContextKey) - if cacheV == nil { - panic(fmt.Errorf("context does not contain a %q value", authzCacheContextKey)) - } - cache, ok := cacheV.(*AuthzCache) - if !ok { - panic(fmt.Errorf("context value %q is a %T, expected %T", - authzCacheContextKey, cacheV, NewAuthzCache())) - } - return cache -} - // UnwrapMetadataContext retrieves a Context from a context.Context instance attached with WrapSDKContext. -// It then adds an AuthzCache to it. -// It panics if a Context was not properly attached, or if the AuthzCache can't be added. +// It then adds an types.AuthzCache to it. +// It panics if a Context was not properly attached, or if the types.AuthzCache can't be added. // // This should be used for all Metadata msg server endpoints instead of sdk.UnwrapSDKContext. // This should not be used outside of the Metadata module. func UnwrapMetadataContext(goCtx context.Context) sdk.Context { - return AddAuthzCacheToContext(sdk.UnwrapSDKContext(goCtx)) -} - -// UsedSignersMap is a type for recording that a signer has been used. -type UsedSignersMap map[string]bool - -// NewUsedSignersMap creates a new UsedSignersMap -func NewUsedSignersMap() UsedSignersMap { - return make(UsedSignersMap) -} - -// Use notes that the provided addresses have been used. -func (m UsedSignersMap) Use(addrs ...string) UsedSignersMap { - for _, addr := range addrs { - m[addr] = true - } - return m -} - -// IsUsed returns true if the provided address has been used. -func (m UsedSignersMap) IsUsed(addr string) bool { - return m[addr] -} - -// AlsoUse adds all the entries in the provided UsedSignersMap to this UsedSignersMap. -func (m UsedSignersMap) AlsoUse(m2 UsedSignersMap) UsedSignersMap { - for k := range m2 { - m[k] = true - } - return m -} - -// findMissing returns all elements of the required list that are not found in the entries list. -// -// See also: findMissingComp -func findMissing(required, toCheck []string) []string { - return findMissingComp(required, toCheck, func(r, c string) bool { return r == c }) -} - -// findMissingParties returns all parties from the required list that don't have a same party in the toCheck list. -// -// See also: findMissingComp -func findMissingParties(required, toCheck []types.Party) []types.Party { - return findMissingComp(required, toCheck, func(r, c types.Party) bool { return types.SamePartiers(&r, &c) }) -} - -// findMissingComp returns all entries in required where an entry does not exist in toCheck -// such that the provided comp function returns true. -// Duplicate entries in required do not require duplicate entries in toCheck. -// E.g. findMissingComp([a, b, a], [a]) => [b], and findMissingComp([a, b, a], [b]) => [a, a]. -func findMissingComp[R any, C any](required []R, toCheck []C, comp func(R, C) bool) []R { - var rv []R -reqLoop: - for _, req := range required { - for _, entry := range toCheck { - if comp(req, entry) { - continue reqLoop - } - } - rv = append(rv, req) - } - return rv -} - -// pluralEnding returns "" if i == 1, or "s" otherwise. -func pluralEnding(i int) string { - if i == 1 { - return "" - } - return "s" + return types.AddAuthzCacheToContext(sdk.UnwrapSDKContext(goCtx)) } // safeBech32ToAccAddresses attempts to convert all provided strings to AccAddresses. diff --git a/x/metadata/keeper/signers_utils_test.go b/x/metadata/keeper/signers_utils_test.go index 6fd6f461b8..be1e90de90 100644 --- a/x/metadata/keeper/signers_utils_test.go +++ b/x/metadata/keeper/signers_utils_test.go @@ -2,2958 +2,111 @@ package keeper_test import ( "context" - "fmt" - "sort" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/authz" "github.com/provenance-io/provenance/x/metadata/keeper" "github.com/provenance-io/provenance/x/metadata/types" ) -// stringSame is a string with an IsSameAs(stringSame) function. -type stringSame string - -// IsSameAs satisfies the sameable interface. -func (s stringSame) IsSameAs(c stringSame) bool { - return string(s) == string(c) -} - -// newStringSames converts a slice of strings to a slice of stringEqs. -// nil in => nil out. empty in => empty out. -func newStringSames(strs []string) []stringSame { - if strs == nil { - return nil - } - rv := make([]stringSame, len(strs), cap(strs)) - for i, str := range strs { - rv[i] = stringSame(str) - } - return rv -} - -// stringSameR is a string with an Equals(stringSameC) function that satisfies the sameable interface using -// different types for the receiver and argument. -type stringSameR string - -// stringSameC is a string that can be provided to the stringSameR IsSameAs function. -type stringSameC string - -// IsSameAs satisfies the sameable interface. -func (s stringSameR) IsSameAs(c stringSameC) bool { - return string(s) == string(c) -} - -// newStringSameRs converts a slice of strings to a slice of stringEqRs. -// nil in => nil out. empty in => empty out. -func newStringSameRs(strs []string) []stringSameR { - if strs == nil { - return nil - } - rv := make([]stringSameR, len(strs), cap(strs)) - for i, str := range strs { - rv[i] = stringSameR(str) - } - return rv -} - -// newStringSameCs converts a slice of strings to a slice of stringEqCs. -// nil in => nil out. empty in => empty out. -func newStringSameCs(strs []string) []stringSameC { - if strs == nil { - return nil - } - rv := make([]stringSameC, len(strs), cap(strs)) - for i, str := range strs { - rv[i] = stringSameC(str) - } - return rv -} - -// partiesCopy creates a new []*keeper.PartyDetails with copies of each provided entry. -// Nil in = nil out. -func partiesCopy(parties []*keeper.PartyDetails) []*keeper.PartyDetails { - if parties == nil { - return nil - } - rv := make([]*keeper.PartyDetails, len(parties)) - for i, party := range parties { - rv[i] = party.Copy() - } - return rv -} - -// partiesReversed creates a new []*keeper.PartyDetails with copies of each provided entry -// in the opposite order as provided. Nil in = nil out. -func partiesReversed(parties []*keeper.PartyDetails) []*keeper.PartyDetails { - if parties == nil { - return nil - } - rv := make([]*keeper.PartyDetails, len(parties)) - for i, party := range parties { - rv[len(rv)-i-1] = party.Copy() - } - return rv -} - func emptySdkContext() sdk.Context { return sdk.Context{}.WithContext(context.Background()) } -func TestWrapRequiredParty(t *testing.T) { - addr := sdk.AccAddress("just_a_test_address_").String() - tests := []struct { - name string - party types.Party - exp *keeper.PartyDetails - }{ - { - name: "control", - party: types.Party{ - Address: addr, - Role: types.PartyType_PARTY_TYPE_OWNER, - Optional: true, - }, - exp: keeper.TestablePartyDetails{ - Address: addr, - Role: types.PartyType_PARTY_TYPE_OWNER, - Optional: true, - }.Real(), - }, - { - name: "zero", - party: types.Party{}, - exp: keeper.TestablePartyDetails{}.Real(), - }, - { - name: "address only", - party: types.Party{Address: addr}, - exp: keeper.TestablePartyDetails{Address: addr}.Real(), - }, - { - name: "role only", - party: types.Party{Role: types.PartyType_PARTY_TYPE_INVESTOR}, - exp: keeper.TestablePartyDetails{Role: types.PartyType_PARTY_TYPE_INVESTOR}.Real(), - }, - { - name: "optional only", - party: types.Party{Optional: true}, - exp: keeper.TestablePartyDetails{Optional: true}.Real(), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := keeper.WrapRequiredParty(tc.party) - assert.Equal(t, tc.exp, actual, "WrapRequiredParty") - }) - } -} - -func TestWrapAvailableParty(t *testing.T) { - addr := sdk.AccAddress("just_a_test_address_").String() - tests := []struct { - name string - party types.Party - exp *keeper.PartyDetails - }{ - { - name: "control", - party: types.Party{ - Address: addr, - Role: types.PartyType_PARTY_TYPE_OWNER, - Optional: true, - }, - exp: keeper.TestablePartyDetails{ - Address: addr, - Role: types.PartyType_PARTY_TYPE_OWNER, - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - }, - { - name: "zero", - party: types.Party{}, - exp: keeper.TestablePartyDetails{ - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - }, - { - name: "address only", - party: types.Party{Address: addr}, - exp: keeper.TestablePartyDetails{ - Address: addr, - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - }, - { - name: "role only", - party: types.Party{Role: types.PartyType_PARTY_TYPE_INVESTOR}, - exp: keeper.TestablePartyDetails{ - Role: types.PartyType_PARTY_TYPE_INVESTOR, - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - }, - { - name: "optional only", - party: types.Party{Optional: true}, - exp: keeper.TestablePartyDetails{ - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := keeper.WrapAvailableParty(tc.party) - assert.Equal(t, tc.exp, actual, "WrapAvailableParty") - }) - } -} - -func TestBuildPartyDetails(t *testing.T) { - addr1 := sdk.AccAddress("this_is_address_1___").String() - addr2 := sdk.AccAddress("this_is_address_2___").String() - addr3 := sdk.AccAddress("this_is_address_3___").String() +func TestSignersWrapper(t *testing.T) { + addr1Acc := sdk.AccAddress("address_one_________") + addr2Acc := sdk.AccAddress("address_one_________") + addr1 := addr1Acc.String() + addr2 := addr2Acc.String() - // pz is a short way to create a slice of parties. - pz := func(parties ...types.Party) []types.Party { - rv := make([]types.Party, 0, len(parties)) - rv = append(rv, parties...) + strz := func(strings ...string) []string { + rv := make([]string, 0, len(strings)) + rv = append(rv, strings...) return rv } - // dz is a short way to create a slice of PartyDetails - pdz := func(parties ...*keeper.PartyDetails) []*keeper.PartyDetails { - rv := make([]*keeper.PartyDetails, 0, len(parties)) - rv = append(rv, parties...) + accz := func(accs ...sdk.AccAddress) []sdk.AccAddress { + rv := make([]sdk.AccAddress, 0, len(accs)) + rv = append(rv, accs...) return rv } - tests := []struct { - name string - reqParties []types.Party - availableParties []types.Party - exp []*keeper.PartyDetails - }{ - { - name: "nil nil", - reqParties: nil, - availableParties: nil, - exp: pdz(), - }, - { - name: "nil empty", - reqParties: nil, - availableParties: pz(), - exp: pdz(), - }, - { - name: "nil one", - reqParties: nil, - availableParties: pz(types.Party{Address: addr1, Role: 3, Optional: false}), - exp: pdz(keeper.TestablePartyDetails{ - Address: addr1, - Role: 3, - Optional: true, - CanBeUsedBySpec: true, - }.Real()), - }, - { - name: "empty nil", - reqParties: pz(), - availableParties: nil, - exp: pdz(), - }, - { - name: "empty empty", - reqParties: pz(), - availableParties: pz(), - exp: pdz(), - }, - { - name: "empty one", - reqParties: pz(), - availableParties: pz(types.Party{Address: addr1, Role: 3, Optional: false}), - exp: pdz(keeper.TestablePartyDetails{ - Address: addr1, - Role: 3, - Optional: true, - CanBeUsedBySpec: true, - }.Real()), - }, - { - name: "one nil", - reqParties: pz(types.Party{Address: addr1, Role: 5, Optional: false}), - availableParties: nil, - exp: pdz(keeper.TestablePartyDetails{ - Address: addr1, - Role: 5, - Optional: false, - }.Real()), - }, - { - name: "one empty", - reqParties: pz(types.Party{Address: addr1, Role: 5, Optional: false}), - availableParties: pz(), - exp: pdz(keeper.TestablePartyDetails{ - Address: addr1, - Role: 5, - Optional: false, - }.Real()), - }, - { - name: "one one different role and address", - reqParties: pz(types.Party{Address: addr1, Role: 5, Optional: false}), - availableParties: pz(types.Party{Address: addr2, Role: 4, Optional: false}), - exp: pdz( - keeper.TestablePartyDetails{ - Address: addr2, - Role: 4, - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - keeper.TestablePartyDetails{ - Address: addr1, - Role: 5, - Optional: false, - }.Real(), - ), - }, - { - name: "one one different role same address", - reqParties: pz(types.Party{Address: addr1, Role: 5, Optional: false}), - availableParties: pz(types.Party{Address: addr1, Role: 4, Optional: false}), - exp: pdz( - keeper.TestablePartyDetails{ - Address: addr1, - Role: 4, - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - keeper.TestablePartyDetails{ - Address: addr1, - Role: 5, - Optional: false, - }.Real(), - ), - }, - { - name: "one one different address same role", - reqParties: pz(types.Party{Address: addr1, Role: 5, Optional: false}), - availableParties: pz(types.Party{Address: addr2, Role: 5, Optional: false}), - exp: pdz( - keeper.TestablePartyDetails{ - Address: addr2, - Role: 5, - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - keeper.TestablePartyDetails{ - Address: addr1, - Role: 5, - Optional: false, - }.Real(), - ), - }, - { - name: "one one same address and role", - reqParties: pz(types.Party{Address: addr1, Role: 5, Optional: false}), - availableParties: pz(types.Party{Address: addr1, Role: 5, Optional: true}), - exp: pdz(keeper.TestablePartyDetails{ - Address: addr1, - Role: 5, - Optional: false, - CanBeUsedBySpec: true, - }.Real()), - }, - { - name: "two two with one same", - reqParties: pz( - types.Party{Address: addr3, Role: 1, Optional: false}, - types.Party{Address: addr2, Role: 7, Optional: false}, - ), - availableParties: pz( - types.Party{Address: addr1, Role: 5, Optional: true}, - types.Party{Address: addr2, Role: 7, Optional: true}, - ), - exp: pdz( - keeper.TestablePartyDetails{ - Address: addr1, - Role: 5, - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - keeper.TestablePartyDetails{ - Address: addr2, - Role: 7, - Optional: false, - CanBeUsedBySpec: true, - }.Real(), - keeper.TestablePartyDetails{ - Address: addr3, - Role: 1, - Optional: false, - }.Real(), - ), - }, - { - name: "duplicate req parties", - reqParties: pz( - types.Party{Address: addr1, Role: 2, Optional: false}, - types.Party{Address: addr1, Role: 2, Optional: false}, - ), - availableParties: nil, - exp: pdz(keeper.TestablePartyDetails{ - Address: addr1, - Role: 2, - Optional: false, - }.Real()), - }, - { - name: "duplicate available parties", - reqParties: nil, - availableParties: pz( - types.Party{Address: addr1, Role: 3, Optional: false}, - types.Party{Address: addr1, Role: 3, Optional: false}, - ), - exp: pdz(keeper.TestablePartyDetails{ - Address: addr1, - Role: 3, - Optional: true, - CanBeUsedBySpec: true, - }.Real()), - }, - { - name: "two req parties one optional", - reqParties: pz( - types.Party{Address: addr1, Role: 2, Optional: false}, - types.Party{Address: addr2, Role: 3, Optional: true}, - ), - availableParties: nil, - exp: pdz(keeper.TestablePartyDetails{ - Address: addr1, - Role: 2, - Optional: false, - }.Real()), - }, - { - name: "two req parties one optional also in available", - reqParties: pz( - types.Party{Address: addr1, Role: 2, Optional: false}, - types.Party{Address: addr2, Role: 3, Optional: true}, - ), - availableParties: pz(types.Party{Address: addr2, Role: 3, Optional: false}), - exp: pdz( - keeper.TestablePartyDetails{ - Address: addr2, - Role: 3, - Optional: true, - CanBeUsedBySpec: true, - }.Real(), - keeper.TestablePartyDetails{ - Address: addr1, - Role: 2, - Optional: false, - }.Real(), - ), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := keeper.BuildPartyDetails(tc.reqParties, tc.availableParties) - assert.Equal(t, tc.exp, actual, "BuildPartyDetails") - }) - } -} - -func TestPartyDetails_SetAddress(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(address string, acc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Address: address, - Acc: acc, - }.Real() - } - - addrAcc := sdk.AccAddress("settable_tst_address") - addr := addrAcc.String() - - tests := []struct { - name string - party *keeper.PartyDetails - addr string - expParty *keeper.PartyDetails - }{ - { - name: "unset to set", - party: pd("", nil), - addr: addr, - expParty: pd(addr, nil), - }, - { - name: "set to unset", - party: pd(addr, addrAcc), - addr: "", - expParty: pd("", nil), - }, - { - name: "changing to non-acc", - party: pd(addr, addrAcc), - addr: "new-address", - expParty: pd("new-address", nil), - }, - { - name: "changing from non-acc", - party: pd("not-an-acc", addrAcc), - addr: addr, - expParty: pd(addr, nil), - }, - { - name: "not changing", - party: pd(addr, sdk.AccAddress("something else")), - addr: addr, - expParty: pd(addr, sdk.AccAddress("something else")), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tc.party.SetAddress(tc.addr) - assert.Equal(t, tc.expParty, tc.party, "party after SetAddress") - }) - } -} - -func TestPartyDetails_GetAddress(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(address string, acc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Address: address, - Acc: acc, - }.Real() - } - - addrAcc := sdk.AccAddress("settable_tst_address") - addr := addrAcc.String() - - tests := []struct { - name string - party *keeper.PartyDetails - exp string - expParty *keeper.PartyDetails - }{ - { - name: "no address no acc", - party: pd("", nil), - exp: "", - expParty: pd("", nil), - }, - { - name: "address without acc", - party: pd(addr, nil), - exp: addr, - expParty: pd(addr, nil), - }, - { - name: "invalid address without acc", - party: pd("invalid", nil), - exp: "invalid", - expParty: pd("invalid", nil), - }, - { - name: "invalid address with acc", - party: pd("invalid", addrAcc), - exp: "invalid", - expParty: pd("invalid", addrAcc), - }, - { - name: "acc without address", - party: pd("", addrAcc), - exp: addr, - expParty: pd(addr, addrAcc), - }, - { - name: "address with different acc", - party: pd(addr, sdk.AccAddress("different_acc_______")), - exp: addr, - expParty: pd(addr, sdk.AccAddress("different_acc_______")), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.GetAddress() - assert.Equal(t, tc.exp, actual, "GetAddress") - assert.Equal(t, tc.expParty, tc.party, "party after GetAddress") - }) - } -} - -func TestPartyDetails_SetAcc(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(address string, acc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Address: address, - Acc: acc, - }.Real() - } - - addrAcc := sdk.AccAddress("settable_tst_address") - addr := addrAcc.String() - - tests := []struct { - name string - party *keeper.PartyDetails - addr sdk.AccAddress - expParty *keeper.PartyDetails - }{ - { - name: "unset to set", - party: pd("", nil), - addr: addrAcc, - expParty: pd("", addrAcc), - }, - { - name: "set to unset", - party: pd(addr, addrAcc), - addr: nil, - expParty: pd("", nil), - }, - { - name: "changing no address", - party: pd("", addrAcc), - addr: sdk.AccAddress("new_address_________"), - expParty: pd("", sdk.AccAddress("new_address_________")), - }, - { - name: "changing have address", - party: pd(addr, addrAcc), - addr: sdk.AccAddress("new_address_________"), - expParty: pd("", sdk.AccAddress("new_address_________")), - }, - { - name: "not changing", - party: pd("something else", addrAcc), - addr: addrAcc, - expParty: pd("something else", addrAcc), - }, - { - name: "nil to empty", - party: pd("foo", nil), - addr: sdk.AccAddress{}, - expParty: pd("foo", sdk.AccAddress{}), - }, - { - name: "empty to nil", - party: pd("foo", sdk.AccAddress{}), - addr: nil, - expParty: pd("foo", nil), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tc.party.SetAcc(tc.addr) - assert.Equal(t, tc.expParty, tc.party, "party after SetAcc") - }) - } -} - -func TestPartyDetails_GetAcc(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(address string, acc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Address: address, - Acc: acc, - }.Real() - } - - addrAcc := sdk.AccAddress("settable_tst_address") - addr := addrAcc.String() tests := []struct { - name string - party *keeper.PartyDetails - exp sdk.AccAddress - expParty *keeper.PartyDetails + name string + wrapper *keeper.SignersWrapper + expStrings []string + expAccs []sdk.AccAddress }{ { - name: "no address nil acc", - party: pd("", nil), - exp: nil, - expParty: pd("", nil), + name: "nil strings", + wrapper: keeper.NewSignersWrapper(nil), + expStrings: nil, + expAccs: accz(), }, { - name: "no address empty acc", - party: pd("", sdk.AccAddress{}), - exp: sdk.AccAddress{}, - expParty: pd("", sdk.AccAddress{}), + name: "empty strings", + wrapper: keeper.NewSignersWrapper(strz()), + expStrings: strz(), + expAccs: accz(), }, { - name: "address without acc", - party: pd(addr, nil), - exp: addrAcc, - expParty: pd(addr, addrAcc), + name: "two valid address", + wrapper: keeper.NewSignersWrapper(strz(addr1, addr2)), + expStrings: strz(addr1, addr2), + expAccs: accz(addr1Acc, addr2Acc), }, { - name: "invalid address without acc", - party: pd("invalid", nil), - exp: nil, - expParty: pd("invalid", nil), + name: "two invalid addresses", + wrapper: keeper.NewSignersWrapper(strz("bad1", "bad2")), + expStrings: strz("bad1", "bad2"), + expAccs: accz(), }, { - name: "invalid address with acc", - party: pd("invalid", addrAcc), - exp: addrAcc, - expParty: pd("invalid", addrAcc), + name: "three addresses first invalid", + wrapper: keeper.NewSignersWrapper(strz("bad1", addr1, addr2)), + expStrings: strz("bad1", addr1, addr2), + expAccs: accz(addr1Acc, addr2Acc), }, { - name: "acc without address", - party: pd("", addrAcc), - exp: addrAcc, - expParty: pd("", addrAcc), + name: "three addresses second invalid", + wrapper: keeper.NewSignersWrapper(strz(addr1, "bad2", addr2)), + expStrings: strz(addr1, "bad2", addr2), + expAccs: accz(addr1Acc, addr2Acc), }, { - name: "address with different acc", - party: pd(addr, sdk.AccAddress("different_acc_______")), - exp: sdk.AccAddress("different_acc_______"), - expParty: pd(addr, sdk.AccAddress("different_acc_______")), + name: "three addresses third invalid", + wrapper: keeper.NewSignersWrapper(strz(addr1, addr2, "bad3")), + expStrings: strz(addr1, addr2, "bad3"), + expAccs: accz(addr1Acc, addr2Acc), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - actual := tc.party.GetAcc() - assert.Equal(t, tc.exp, actual, "GetAcc") - assert.Equal(t, tc.expParty, tc.party, "party after GetAcc") + actualStrings := tc.wrapper.Strings() + assert.Equal(t, tc.expStrings, actualStrings, ".String()") + actualAccs := tc.wrapper.Accs() + assert.Equal(t, tc.expAccs, actualAccs, ".Accs()") }) } } -func TestPartyDetails_SetRole(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(role types.PartyType) *keeper.PartyDetails { - return keeper.TestablePartyDetails{Role: role}.Real() +func TestUnwrapMetadataContext(t *testing.T) { + origCtx := emptySdkContext() + var ctx sdk.Context + testUnwrap := func() { + ctx = keeper.UnwrapMetadataContext(origCtx) } - - tests := []struct { - name string - party *keeper.PartyDetails - role types.PartyType - expParty *keeper.PartyDetails - }{ - { - name: "unset to set", - party: pd(0), - role: 1, - expParty: pd(1), - }, - { - name: "set to unset", - party: pd(2), - role: 0, - expParty: pd(0), - }, - { - name: "changing", - party: pd(3), - role: 8, - expParty: pd(8), - }, - { - name: "not changing", - party: pd(4), - role: 4, - expParty: pd(4), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tc.party.SetRole(tc.role) - assert.Equal(t, tc.expParty, tc.party, "party after SetRole") - }) - } -} - -func TestPartyDetails_GetRole(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(role types.PartyType) *keeper.PartyDetails { - return keeper.TestablePartyDetails{Role: role}.Real() - } - - type testCase struct { - name string - party *keeper.PartyDetails - exp types.PartyType - } - - var tests []testCase - for r := range types.PartyType_name { - role := types.PartyType(r) - tests = append(tests, testCase{ - name: role.SimpleString(), - party: pd(role), - exp: role, - }) - } - sort.Slice(tests, func(i, j int) bool { - return tests[i].party.GetRole() < tests[j].party.GetRole() - }) - tests = append(tests, testCase{ - name: "invalid role", - party: pd(-8), - exp: -8, - }) - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.GetRole() - assert.Equal(t, tc.exp.SimpleString(), actual.SimpleString(), "GetRole") - }) - } -} - -func TestPartyDetails_SetOptional(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(optional bool) *keeper.PartyDetails { - return keeper.TestablePartyDetails{Optional: optional}.Real() - } - - tests := []struct { - name string - party *keeper.PartyDetails - optional bool - expParty *keeper.PartyDetails - }{ - { - name: "true to true", - party: pd(true), - optional: true, - expParty: pd(true), - }, - { - name: "true to false", - party: pd(true), - optional: false, - expParty: pd(false), - }, - { - name: "false to true", - party: pd(false), - optional: true, - expParty: pd(true), - }, - { - name: "false to false", - party: pd(false), - optional: false, - expParty: pd(false), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tc.party.SetOptional(tc.optional) - assert.Equal(t, tc.expParty, tc.party, "party after SetOptional") - }) - } -} - -func TestPartyDetails_MakeRequired(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(optional bool) *keeper.PartyDetails { - return keeper.TestablePartyDetails{Optional: optional}.Real() - } - - tests := []struct { - name string - party *keeper.PartyDetails - expParty *keeper.PartyDetails - }{ - { - name: "from optional", - party: pd(true), - expParty: pd(false), - }, - { - name: "from required", - party: pd(false), - expParty: pd(false), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tc.party.MakeRequired() - assert.Equal(t, tc.expParty, tc.party, "party after MakeRequired") - }) - } -} - -func TestPartyDetails_GetOptional(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(optional bool) *keeper.PartyDetails { - return keeper.TestablePartyDetails{Optional: optional}.Real() - } - - tests := []struct { - name string - party *keeper.PartyDetails - exp bool - expParty *keeper.PartyDetails - }{ - { - name: "optional", - party: pd(true), - exp: true, - expParty: pd(true), - }, - { - name: "required", - party: pd(false), - exp: false, - expParty: pd(false), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.GetOptional() - assert.Equal(t, tc.exp, actual, "GetOptional") - assert.Equal(t, tc.expParty, tc.party, "party after GetOptional") - }) - } -} - -func TestPartyDetails_IsRequired(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(optional bool) *keeper.PartyDetails { - return keeper.TestablePartyDetails{Optional: optional}.Real() - } - - tests := []struct { - name string - party *keeper.PartyDetails - exp bool - expParty *keeper.PartyDetails - }{ - { - name: "optional", - party: pd(true), - exp: false, - expParty: pd(true), - }, - { - name: "required", - party: pd(false), - exp: true, - expParty: pd(false), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.IsRequired() - assert.Equal(t, tc.exp, actual, "IsRequired") - assert.Equal(t, tc.expParty, tc.party, "party after IsRequired") - }) - } -} - -func TestPartyDetails_SetSigner(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(signer string, signerAcc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Signer: signer, - SignerAcc: signerAcc, - }.Real() - } - - addrAcc := sdk.AccAddress("settable_tst_address") - addr := addrAcc.String() - - tests := []struct { - name string - party *keeper.PartyDetails - signer string - expParty *keeper.PartyDetails - }{ - { - name: "unset to set", - party: pd("", nil), - signer: addr, - expParty: pd(addr, nil), - }, - { - name: "set to unset", - party: pd(addr, addrAcc), - signer: "", - expParty: pd("", nil), - }, - { - name: "changing to non-acc", - party: pd(addr, addrAcc), - signer: "new-address", - expParty: pd("new-address", nil), - }, - { - name: "changing from non-acc", - party: pd("not-an-acc", addrAcc), - signer: addr, - expParty: pd(addr, nil), - }, - { - name: "not changing", - party: pd(addr, sdk.AccAddress("something else")), - signer: addr, - expParty: pd(addr, sdk.AccAddress("something else")), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tc.party.SetSigner(tc.signer) - assert.Equal(t, tc.expParty, tc.party, "party after SetSigner") - }) - } -} - -func TestPartyDetails_GetSigner(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(signer string, signerAcc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Signer: signer, - SignerAcc: signerAcc, - }.Real() - } - - addrAcc := sdk.AccAddress("settable_tst_address") - addr := addrAcc.String() - - tests := []struct { - name string - party *keeper.PartyDetails - exp string - expParty *keeper.PartyDetails - }{ - { - name: "no address no acc", - party: pd("", nil), - exp: "", - expParty: pd("", nil), - }, - { - name: "address without acc", - party: pd(addr, nil), - exp: addr, - expParty: pd(addr, nil), - }, - { - name: "invalid address without acc", - party: pd("invalid", nil), - exp: "invalid", - expParty: pd("invalid", nil), - }, - { - name: "invalid address with acc", - party: pd("invalid", addrAcc), - exp: "invalid", - expParty: pd("invalid", addrAcc), - }, - { - name: "acc without address", - party: pd("", addrAcc), - exp: addr, - expParty: pd(addr, addrAcc), - }, - { - name: "address with different acc", - party: pd(addr, sdk.AccAddress("different_acc_______")), - exp: addr, - expParty: pd(addr, sdk.AccAddress("different_acc_______")), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.GetSigner() - assert.Equal(t, tc.exp, actual, "GetSigner") - assert.Equal(t, tc.expParty, tc.party, "party after GetSigner") - }) - } -} - -func TestPartyDetails_SetSignerAcc(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(signer string, signerAcc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Signer: signer, - SignerAcc: signerAcc, - }.Real() - } - - addrAcc := sdk.AccAddress("settable_tst_address") - addr := addrAcc.String() - - tests := []struct { - name string - party *keeper.PartyDetails - signer sdk.AccAddress - expParty *keeper.PartyDetails - }{ - { - name: "unset to set", - party: pd("", nil), - signer: addrAcc, - expParty: pd("", addrAcc), - }, - { - name: "set to unset", - party: pd(addr, addrAcc), - signer: nil, - expParty: pd("", nil), - }, - { - name: "changing no address", - party: pd("", addrAcc), - signer: sdk.AccAddress("new_address_________"), - expParty: pd("", sdk.AccAddress("new_address_________")), - }, - { - name: "changing have address", - party: pd(addr, addrAcc), - signer: sdk.AccAddress("new_address_________"), - expParty: pd("", sdk.AccAddress("new_address_________")), - }, - { - name: "not changing", - party: pd("something else", addrAcc), - signer: addrAcc, - expParty: pd("something else", addrAcc), - }, - { - name: "nil to empty", - party: pd("foo", nil), - signer: sdk.AccAddress{}, - expParty: pd("foo", sdk.AccAddress{}), - }, - { - name: "empty to nil", - party: pd("foo", sdk.AccAddress{}), - signer: nil, - expParty: pd("foo", nil), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tc.party.SetSignerAcc(tc.signer) - assert.Equal(t, tc.expParty, tc.party, "party after SetSignerAcc") - }) - } -} - -func TestPartyDetails_GetSignerAcc(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(signer string, signerAcc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Signer: signer, - SignerAcc: signerAcc, - }.Real() - } - - addrAcc := sdk.AccAddress("settable_tst_address") - addr := addrAcc.String() - - tests := []struct { - name string - party *keeper.PartyDetails - exp sdk.AccAddress - expParty *keeper.PartyDetails - }{ - { - name: "no address nil acc", - party: pd("", nil), - exp: nil, - expParty: pd("", nil), - }, - { - name: "no address empty acc", - party: pd("", sdk.AccAddress{}), - exp: sdk.AccAddress{}, - expParty: pd("", sdk.AccAddress{}), - }, - { - name: "address without acc", - party: pd(addr, nil), - exp: addrAcc, - expParty: pd(addr, addrAcc), - }, - { - name: "invalid address without acc", - party: pd("invalid", nil), - exp: nil, - expParty: pd("invalid", nil), - }, - { - name: "invalid address with acc", - party: pd("invalid", addrAcc), - exp: addrAcc, - expParty: pd("invalid", addrAcc), - }, - { - name: "acc without address", - party: pd("", addrAcc), - exp: addrAcc, - expParty: pd("", addrAcc), - }, - { - name: "address with different acc", - party: pd(addr, sdk.AccAddress("different_acc_______")), - exp: sdk.AccAddress("different_acc_______"), - expParty: pd(addr, sdk.AccAddress("different_acc_______")), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.GetSignerAcc() - assert.Equal(t, tc.exp, actual, "GetSignerAcc") - assert.Equal(t, tc.expParty, tc.party, "party after GetSignerAcc") - }) - } -} - -func TestPartyDetails_HasSigner(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(signer string, signerAcc sdk.AccAddress) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Signer: signer, - SignerAcc: signerAcc, - }.Real() - } - - tests := []struct { - name string - party *keeper.PartyDetails - exp bool - expParty *keeper.PartyDetails - }{ - { - name: "no string or acc", - party: pd("", nil), - exp: false, - expParty: pd("", nil), - }, - { - name: "string no acc", - party: pd("a", nil), - exp: true, - expParty: pd("a", nil), - }, - { - name: "acc no string", - party: pd("", sdk.AccAddress("b")), - exp: true, - expParty: pd("", sdk.AccAddress("b")), - }, - { - name: "string and acc", - party: pd("a", sdk.AccAddress("b")), - exp: true, - expParty: pd("a", sdk.AccAddress("b")), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.HasSigner() - assert.Equal(t, tc.exp, actual, "HasSigner") - assert.Equal(t, tc.expParty, tc.party, "party after HasSigner") - }) - } -} - -func TestPartyDetails_CanBeUsed(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(canBeUsedBySpec bool) *keeper.PartyDetails { - return keeper.TestablePartyDetails{CanBeUsedBySpec: canBeUsedBySpec}.Real() - } - - tests := []struct { - name string - party *keeper.PartyDetails - exp bool - expParty *keeper.PartyDetails - }{ - { - name: "can be used", - party: pd(true), - exp: true, - expParty: pd(true), - }, - { - name: "cannot be used", - party: pd(false), - exp: false, - expParty: pd(false), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.CanBeUsed() - assert.Equal(t, tc.exp, actual, "CanBeUsed") - assert.Equal(t, tc.expParty, tc.party, "party after CanBeUsed") - }) - } -} - -func TestPartyDetails_MarkAsUsed(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(usedBySpec bool) *keeper.PartyDetails { - return keeper.TestablePartyDetails{UsedBySpec: usedBySpec}.Real() - } - - tests := []struct { - name string - party *keeper.PartyDetails - expParty *keeper.PartyDetails - }{ - { - name: "from not used", - party: pd(false), - expParty: pd(true), - }, - { - name: "from used", - party: pd(true), - expParty: pd(true), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tc.party.MarkAsUsed() - assert.Equal(t, tc.expParty, tc.party, "party after MarkAsUsed") - }) - } -} - -func TestPartyDetails_IsUsed(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(usedBySpec bool) *keeper.PartyDetails { - return keeper.TestablePartyDetails{UsedBySpec: usedBySpec}.Real() - } - - tests := []struct { - name string - party *keeper.PartyDetails - exp bool - expParty *keeper.PartyDetails - }{ - { - name: "used", - party: pd(true), - exp: true, - expParty: pd(true), - }, - { - name: "not used", - party: pd(false), - exp: false, - expParty: pd(false), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.IsUsed() - assert.Equal(t, tc.exp, actual, "IsUsed") - assert.Equal(t, tc.expParty, tc.party, "party after IsUsed") - }) - } -} - -func TestPartyDetails_IsStillUsableAs(t *testing.T) { - // pd is a short way to create a PartyDetails with only what we care about in this test. - pd := func(role types.PartyType, canBeUsedBySpec, usedBySpec bool) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Role: role, - CanBeUsedBySpec: canBeUsedBySpec, - UsedBySpec: usedBySpec, - }.Real() - } - - tests := []struct { - name string - party *keeper.PartyDetails - role types.PartyType - exp bool - expParty *keeper.PartyDetails - }{ - { - name: "same role can be used is not used", - party: pd(1, true, false), - role: 1, - exp: true, - expParty: pd(1, true, false), - }, - { - name: "same role can be used is used", - party: pd(1, true, true), - role: 1, - exp: false, - expParty: pd(1, true, true), - }, - { - name: "same role cannot be used is not used", - party: pd(1, false, false), - role: 1, - exp: false, - expParty: pd(1, false, false), - }, - { - name: "same role cannot be used is used", - party: pd(1, false, true), - role: 1, - exp: false, - expParty: pd(1, false, true), - }, - { - name: "diff role can be used is not used", - party: pd(1, true, false), - role: 2, - exp: false, - expParty: pd(1, true, false), - }, - { - name: "diff role can be used is used", - party: pd(1, true, true), - role: 2, - exp: false, - expParty: pd(1, true, true), - }, - { - name: "diff role cannot be used is not used", - party: pd(1, false, false), - role: 2, - exp: false, - expParty: pd(1, false, false), - }, - { - name: "diff role cannot be used is used", - party: pd(1, false, true), - role: 2, - exp: false, - expParty: pd(1, false, true), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.IsStillUsableAs(tc.role) - assert.Equal(t, tc.exp, actual, "IsStillUsableAs(%s)", tc.role.SimpleString()) - assert.Equal(t, tc.expParty, tc.party, "party after IsStillUsableAs") - }) - } -} - -func TestPartyDetails_IsSameAs(t *testing.T) { - tests := []struct { - name string - party *keeper.PartyDetails - p2 types.Partier - exp bool - expParty *keeper.PartyDetails - }{ - { - name: "party details same addr and role all others different", - party: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - Acc: sdk.AccAddress("one_________________"), - Signer: "signer1", - SignerAcc: sdk.AccAddress("signer1_____________"), - CanBeUsedBySpec: false, - UsedBySpec: false, - }.Real(), - p2: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: true, - Acc: sdk.AccAddress("two_________________"), - Signer: "signer2", - SignerAcc: sdk.AccAddress("signer2_____________"), - CanBeUsedBySpec: true, - UsedBySpec: true, - }.Real(), - exp: true, - expParty: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - Acc: sdk.AccAddress("one_________________"), - Signer: "signer1", - SignerAcc: sdk.AccAddress("signer1_____________"), - CanBeUsedBySpec: false, - UsedBySpec: false, - }.Real(), - }, - { - name: "party same addr and role different optional", - party: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - p2: &types.Party{ - Address: "same", - Role: 1, - Optional: true, - }, - exp: true, - expParty: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - }, - { - name: "same but only have acc", - party: keeper.TestablePartyDetails{ - Acc: sdk.AccAddress("same_acc_address____"), - Role: 1, - Optional: false, - }.Real(), - p2: &types.Party{ - Address: sdk.AccAddress("same_acc_address____").String(), - Role: 1, - Optional: true, - }, - exp: true, - expParty: keeper.TestablePartyDetails{ - Address: sdk.AccAddress("same_acc_address____").String(), - Acc: sdk.AccAddress("same_acc_address____"), - Role: 1, - Optional: false, - }.Real(), - }, - { - name: "same but both only have acc", - party: keeper.TestablePartyDetails{ - Acc: sdk.AccAddress("same_acc_address____"), - Role: 1, - Optional: false, - }.Real(), - p2: keeper.TestablePartyDetails{ - Acc: sdk.AccAddress("same_acc_address____"), - Role: 1, - Optional: false, - }.Real(), - exp: true, - expParty: keeper.TestablePartyDetails{ - Address: sdk.AccAddress("same_acc_address____").String(), - Acc: sdk.AccAddress("same_acc_address____"), - Role: 1, - Optional: false, - }.Real(), - }, - { - name: "party details different address", - party: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - p2: keeper.TestablePartyDetails{ - Address: "not same", - Role: 1, - Optional: true, - }.Real(), - exp: false, - expParty: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - }, - { - name: "party details different role", - party: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - p2: keeper.TestablePartyDetails{ - Address: "same", - Role: 2, - Optional: true, - }.Real(), - exp: false, - expParty: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - }, - { - name: "party different address", - party: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - p2: &types.Party{ - Address: "not same", - Role: 1, - Optional: true, - }, - exp: false, - expParty: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - }, - { - name: "party different role", - party: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - p2: &types.Party{ - Address: "same", - Role: 2, - Optional: true, - }, - exp: false, - expParty: keeper.TestablePartyDetails{ - Address: "same", - Role: 1, - Optional: false, - }.Real(), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.party.IsSameAs(tc.p2) - assert.Equal(t, tc.exp, actual, "IsSameAs") - assert.Equal(t, tc.expParty, tc.party, "party after IsSameAs") - }) - } -} - -func TestGetUsedSigners(t *testing.T) { - addr := func(str string) sdk.AccAddress { - if len(str) == 0 { - return nil - } - return sdk.AccAddress(str) - } - addrStr := func(str string) string { - if len(str) == 0 { - return "" - } - return addr(str).String() - } - pd := func(address, signer, signerAcc string) *keeper.PartyDetails { - return keeper.TestablePartyDetails{ - Address: addrStr(address), - Signer: addrStr(signer), - SignerAcc: addr(signerAcc), - }.Real() - } - pdz := func(parties ...*keeper.PartyDetails) []*keeper.PartyDetails { - rv := make([]*keeper.PartyDetails, 0, len(parties)) - rv = append(rv, parties...) - return parties - } - - tests := []struct { - name string - parties []*keeper.PartyDetails - exp keeper.UsedSignersMap - }{ - { - name: "nil parties", - parties: nil, - exp: map[string]bool{}, - }, - { - name: "empty parties", - parties: pdz(), - exp: map[string]bool{}, - }, - { - name: "one party no signer", - parties: pdz(pd("addr1", "", "")), - exp: map[string]bool{}, - }, - { - name: "one party signer string", - parties: pdz(pd("addr1", "signer_string", "")), - exp: map[string]bool{addrStr("signer_string"): true}, - }, - { - name: "one party signer acc", - parties: pdz(pd("addr1", "", "signer_acc")), - exp: map[string]bool{addrStr("signer_acc"): true}, - }, - { - name: "one party both signer string and acc", - parties: pdz(pd("addr1", "signer_string", "signer_acc")), - exp: map[string]bool{addrStr("signer_string"): true}, - }, - { - name: "two parties neither have signer", - parties: pdz(pd("addr1", "", ""), pd("addr2", "", "")), - exp: map[string]bool{}, - }, - { - name: "two parties 1st has signer", - parties: pdz(pd("addr1", "signer1", ""), pd("addr2", "", "")), - exp: map[string]bool{addrStr("signer1"): true}, - }, - { - name: "two parties 2nd has signer", - parties: pdz(pd("addr1", "", ""), pd("addr2", "signer2", "")), - exp: map[string]bool{addrStr("signer2"): true}, - }, - { - name: "two parties both have different signer", - parties: pdz(pd("addr1", "signer1", ""), pd("addr2", "signer2", "")), - exp: map[string]bool{addrStr("signer1"): true, addrStr("signer2"): true}, - }, - { - name: "two parties both have same signer", - parties: pdz(pd("addr1", "signer1", ""), pd("addr2", "signer1", "")), - exp: map[string]bool{addrStr("signer1"): true}, - }, - { - name: "five parties, 1 without a signer, 1 with signer str, 1 with same signer acc, 2 with unique signers", - parties: pdz( - pd("addr1", "signer1", ""), - pd("addr2", "", ""), - pd("addr3", "", "signer2"), - pd("addr4", "", "signer1"), - pd("addr5", "signer3", ""), - ), - exp: map[string]bool{ - addrStr("signer1"): true, - addrStr("signer2"): true, - addrStr("signer3"): true, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := keeper.GetUsedSigners(tc.parties) - assert.Equal(t, tc.exp, actual, "GetAllSigners") - }) - } -} - -func TestSignersWrapper(t *testing.T) { - addr1Acc := sdk.AccAddress("address_one_________") - addr2Acc := sdk.AccAddress("address_one_________") - addr1 := addr1Acc.String() - addr2 := addr2Acc.String() - - strz := func(strings ...string) []string { - rv := make([]string, 0, len(strings)) - rv = append(rv, strings...) - return rv - } - accz := func(accs ...sdk.AccAddress) []sdk.AccAddress { - rv := make([]sdk.AccAddress, 0, len(accs)) - rv = append(rv, accs...) - return rv - } - - tests := []struct { - name string - wrapper *keeper.SignersWrapper - expStrings []string - expAccs []sdk.AccAddress - }{ - { - name: "nil strings", - wrapper: keeper.NewSignersWrapper(nil), - expStrings: nil, - expAccs: accz(), - }, - { - name: "empty strings", - wrapper: keeper.NewSignersWrapper(strz()), - expStrings: strz(), - expAccs: accz(), - }, - { - name: "two valid address", - wrapper: keeper.NewSignersWrapper(strz(addr1, addr2)), - expStrings: strz(addr1, addr2), - expAccs: accz(addr1Acc, addr2Acc), - }, - { - name: "two invalid addresses", - wrapper: keeper.NewSignersWrapper(strz("bad1", "bad2")), - expStrings: strz("bad1", "bad2"), - expAccs: accz(), - }, - { - name: "three addresses first invalid", - wrapper: keeper.NewSignersWrapper(strz("bad1", addr1, addr2)), - expStrings: strz("bad1", addr1, addr2), - expAccs: accz(addr1Acc, addr2Acc), - }, - { - name: "three addresses second invalid", - wrapper: keeper.NewSignersWrapper(strz(addr1, "bad2", addr2)), - expStrings: strz(addr1, "bad2", addr2), - expAccs: accz(addr1Acc, addr2Acc), - }, - { - name: "three addresses third invalid", - wrapper: keeper.NewSignersWrapper(strz(addr1, addr2, "bad3")), - expStrings: strz(addr1, addr2, "bad3"), - expAccs: accz(addr1Acc, addr2Acc), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actualStrings := tc.wrapper.Strings() - assert.Equal(t, tc.expStrings, actualStrings, ".String()") - actualAccs := tc.wrapper.Accs() - assert.Equal(t, tc.expAccs, actualAccs, ".Accs()") - - }) - } -} - -func TestAuthzCacheAcceptableKey(t *testing.T) { - grantee := sdk.AccAddress("y_grantee_z") - granter := sdk.AccAddress("Y_GRANTER_Z") - msgTypeURL := "1_msg_type_url_9" - - firstChar := func(str string) string { - return str[0:1] - } - lastChar := func(str string) string { - return str[len(str)-1:] - } - - tests := []struct { - name string - subStr string - contains bool - }{ - { - name: "grantee", - subStr: string(grantee), - contains: true, - }, - { - name: "granter", - subStr: string(granter), - contains: true, - }, - { - name: "msgTypeURL", - subStr: msgTypeURL, - contains: true, - }, - { - name: "grantee last granter first", - subStr: lastChar(string(grantee)) + firstChar(string(granter)), - contains: false, - }, - { - name: "granter last grantee first", - subStr: lastChar(string(granter)) + firstChar(string(grantee)), - contains: false, - }, - { - name: "grantee last msgTypeURL first", - subStr: lastChar(string(grantee)) + firstChar(msgTypeURL), - contains: false, - }, - { - name: "msgTypeURL last grantee first", - subStr: lastChar(msgTypeURL) + firstChar(string(grantee)), - contains: false, - }, - { - name: "granter last msgTypeURL first", - subStr: lastChar(string(granter)) + firstChar(msgTypeURL), - contains: false, - }, - { - name: "msgTypeURL last granter first", - subStr: lastChar(msgTypeURL) + firstChar(string(granter)), - contains: false, - }, - } - - actual := keeper.AuthzCacheAcceptableKey(grantee, granter, msgTypeURL) - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.contains { - assert.Contains(t, actual, tc.subStr, "expected substring of authzCacheAcceptableKey result") - } else { - assert.NotContains(t, actual, tc.subStr, "unexpected substring of authzCacheAcceptableKey result") - } - }) - } -} - -func TestAuthzCacheIsWasmKey(t *testing.T) { - tests := []struct { - name string - str string - }{ - {name: "20 char addr", str: "20_character_address"}, - {name: "32 char addr", str: "thirty_two___character___address"}, - {name: "a space", str: " "}, - {name: "empty", str: ""}, - {name: "bytes 0 to 10", str: string([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10})}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - addr := sdk.AccAddress(tc.str) - actual := keeper.AuthzCacheIsWasmKey(addr) - assert.Equal(t, tc.str, actual, "authzCacheIsWasmKey") - }) - } -} - -func TestNewAuthzCache(t *testing.T) { - c1 := keeper.NewAuthzCache() - c1Type := fmt.Sprintf("%T", c1) - c2 := keeper.NewAuthzCache() - - assert.NotNil(t, c1, "NewAuthzCache result") - assert.Equal(t, "*keeper.AuthzCache", c1Type, "type returned by NewAuthzCache") - assert.Empty(t, c1.AcceptableMap(), "acceptable map") - assert.Empty(t, c1.IsWasmMap(), "isWasm map") - - assert.NotSame(t, c1, c2, "NewAuthzCache twice") - assert.NotSame(t, c1.AcceptableMap(), c2.AcceptableMap(), "acceptable maps of two NewAuthzCache") - assert.NotSame(t, c1.IsWasmMap(), c2.IsWasmMap(), "isWasm maps of two NewAuthzCache") -} - -func TestAuthzCache_Clear(t *testing.T) { - c := keeper.NewAuthzCache() - c.AcceptableMap()["key1"] = &authz.CountAuthorization{} - c.AcceptableMap()["key2"] = &authz.GenericAuthorization{} - c.IsWasmMap()["key3"] = true - c.IsWasmMap()["key4"] = false - assert.NotEmpty(t, c.AcceptableMap(), "AuthzCache acceptable map before clear") - assert.NotEmpty(t, c.IsWasmMap(), "AuthzCache isWasm map before clear") - c.Clear() - assert.Empty(t, c.AcceptableMap(), "AuthzCache acceptable map after clear") - assert.Empty(t, c.IsWasmMap(), "AuthzCache isWasm map after clear") -} - -func TestAuthzCache_SetAcceptable(t *testing.T) { - c := keeper.NewAuthzCache() - grantee := sdk.AccAddress("grantee") - granter := sdk.AccAddress("granter") - msgTypeURL := "msgTypeURL" - authorization := &authz.CountAuthorization{ - Msg: msgTypeURL, - AllowedAuthorizations: 77, - } - - c.SetAcceptable(grantee, granter, msgTypeURL, authorization) - actual := c.AcceptableMap()[keeper.AuthzCacheAcceptableKey(grantee, granter, msgTypeURL)] - assert.Equal(t, authorization, actual, "the authorization stored by SetAcceptable") -} - -func TestAuthzCache_GetAcceptable(t *testing.T) { - c := keeper.NewAuthzCache() - grantee := sdk.AccAddress("grantee") - granter := sdk.AccAddress("granter") - msgTypeURL := "msgTypeURL" - key := keeper.AuthzCacheAcceptableKey(grantee, granter, msgTypeURL) - - authorization := &authz.CountAuthorization{ - Msg: msgTypeURL, - AllowedAuthorizations: 8, - } - c.AcceptableMap()[key] = authorization - - actual := c.GetAcceptable(grantee, granter, msgTypeURL) - assert.Equal(t, authorization, actual, "GetAcceptable result") - - notThere := c.GetAcceptable(granter, grantee, msgTypeURL) - assert.Nil(t, notThere, "GetAcceptable on an entry that should not exist") -} - -func TestAuthzCache_SetIsWasm(t *testing.T) { - c := keeper.NewAuthzCache() - - // These tests will build on eachother using the same AuthzCache. - tests := []struct { - name string - addr sdk.AccAddress - value bool - exp map[string]bool - }{ - { - name: "new entry true", - addr: sdk.AccAddress("addr_true"), - value: true, - exp: map[string]bool{"addr_true": true}, - }, - { - name: "new entry false", - addr: sdk.AccAddress("addr_false"), - value: false, - exp: map[string]bool{"addr_true": true, "addr_false": false}, - }, - { - name: "change true to false", - addr: sdk.AccAddress("addr_true"), - value: false, - exp: map[string]bool{"addr_true": false, "addr_false": false}, - }, - { - name: "change false to true", - addr: sdk.AccAddress("addr_false"), - value: true, - exp: map[string]bool{"addr_true": false, "addr_false": true}, - }, - { - name: "nil address", - addr: nil, - value: true, - exp: map[string]bool{"addr_true": false, "addr_false": true, "": true}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - c.SetIsWasm(tc.addr, tc.value) - m := c.IsWasmMap() - assert.Equal(t, tc.exp, m, "isWasm map after SetIsWasm") - }) - } -} - -func TestAuthzCache_HasIsWasm(t *testing.T) { - c := keeper.NewAuthzCache() - addrTrue := sdk.AccAddress("addrTrue") - addrFalse := sdk.AccAddress("addrFalse") - addrUnknown := sdk.AccAddress("addrUnknown") - c.SetIsWasm(addrTrue, true) - c.SetIsWasm(addrFalse, false) - - tests := []struct { - name string - addr sdk.AccAddress - exp bool - }{ - {name: "known true", addr: addrTrue, exp: true}, - {name: "known false", addr: addrFalse, exp: true}, - {name: "unknown", addr: addrUnknown, exp: false}, - {name: "nil", addr: nil, exp: false}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := c.HasIsWasm(tc.addr) - assert.Equal(t, tc.exp, actual, "HasIsWasm") - }) - } -} - -func TestAuthzCache_GetIsWasm(t *testing.T) { - c := keeper.NewAuthzCache() - addrTrue := sdk.AccAddress("addrTrue") - addrFalse := sdk.AccAddress("addrFalse") - addrUnknown := sdk.AccAddress("addrUnknown") - c.SetIsWasm(addrTrue, true) - c.SetIsWasm(addrFalse, false) - - tests := []struct { - name string - addr sdk.AccAddress - exp bool - }{ - {name: "known true", addr: addrTrue, exp: true}, - {name: "known false", addr: addrFalse, exp: false}, - {name: "unknown", addr: addrUnknown, exp: false}, - {name: "nil", addr: nil, exp: false}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := c.GetIsWasm(tc.addr) - assert.Equal(t, tc.exp, actual, "GetIsWasm") - }) - } -} - -func TestAddAuthzCacheToContext(t *testing.T) { - t.Run("context does not already have the key", func(t *testing.T) { - origCtx := emptySdkContext() - newCtx := keeper.AddAuthzCacheToContext(origCtx) - - cacheOrig := origCtx.Value(keeper.AuthzCacheContextKey) - assert.Nil(t, cacheOrig, "original context %q value", keeper.AuthzCacheContextKey) - - cacheV := newCtx.Value(keeper.AuthzCacheContextKey) - require.NotNil(t, cacheV, "new context %q value", keeper.AuthzCacheContextKey) - cache, ok := cacheV.(*keeper.AuthzCache) - require.True(t, ok, "can cast %q value to *keeper.AuthzCache", keeper.AuthzCacheContextKey) - require.NotNil(t, cache, "the %q value cast to a *keeper.AuthzCache", keeper.AuthzCacheContextKey) - assert.Empty(t, cache.AcceptableMap(), "the acceptable map of the newly added *keeper.AuthzCache") - }) - - t.Run("context already has an AuthzCache", func(t *testing.T) { - grantee := sdk.AccAddress("grantee") - granter := sdk.AccAddress("granter") - msgTypeURL := "msgTypeURL" - authorization := &authz.CountAuthorization{ - Msg: msgTypeURL, - AllowedAuthorizations: 8, - } - origCache := keeper.NewAuthzCache() - origCache.SetAcceptable(grantee, granter, msgTypeURL, authorization) - - origCtx := emptySdkContext().WithValue(keeper.AuthzCacheContextKey, origCache) - newCtx := keeper.AddAuthzCacheToContext(origCtx) - - var newCache *keeper.AuthzCache - testFunc := func() { - newCache = keeper.GetAuthzCache(newCtx) - } - require.NotPanics(t, testFunc, "GetAuthzCache") - assert.Same(t, origCache, newCache, "cache from new context") - assert.Empty(t, newCache.AcceptableMap(), "cache acceptable map") - }) - - t.Run("context has something else", func(t *testing.T) { - origCtx := emptySdkContext().WithValue(keeper.AuthzCacheContextKey, "something else") - - expErr := "context value \"authzCacheContextKey\" is a string, expected *keeper.AuthzCache" - testFunc := func() { - _ = keeper.AddAuthzCacheToContext(origCtx) - } - require.PanicsWithError(t, expErr, testFunc, "AddAuthzCacheToContext") - }) -} - -func TestGetAuthzCache(t *testing.T) { - t.Run("context does not have it", func(t *testing.T) { - ctx := emptySdkContext() - expErr := "context does not contain a \"authzCacheContextKey\" value" - testFunc := func() { - _ = keeper.GetAuthzCache(ctx) - } - require.PanicsWithError(t, expErr, testFunc, "GetAuthzCache") - }) - - t.Run("context has something else", func(t *testing.T) { - ctx := emptySdkContext().WithValue(keeper.AuthzCacheContextKey, "something else") - expErr := "context value \"authzCacheContextKey\" is a string, expected *keeper.AuthzCache" - testFunc := func() { - _ = keeper.GetAuthzCache(ctx) - } - require.PanicsWithError(t, expErr, testFunc, "GetAuthzCache") - }) - - t.Run("context has it", func(t *testing.T) { - origCache := keeper.NewAuthzCache() - origCache.AcceptableMap()["key1"] = &authz.GenericAuthorization{Msg: "msg"} - ctx := emptySdkContext().WithValue(keeper.AuthzCacheContextKey, origCache) - var cache *keeper.AuthzCache - testFunc := func() { - cache = keeper.GetAuthzCache(ctx) - } - require.NotPanics(t, testFunc, "GetAuthzCache") - assert.Same(t, origCache, cache, "cache returned by GetAuthzCache") - }) -} - -func TestUnwrapMetadataContext(t *testing.T) { - origCtx := emptySdkContext() - var ctx sdk.Context - testUnwrap := func() { - ctx = keeper.UnwrapMetadataContext(origCtx) - } - require.NotPanics(t, testUnwrap, "UnwrapMetadataContext") - var cache *keeper.AuthzCache - testGet := func() { - cache = keeper.GetAuthzCache(ctx) + require.NotPanics(t, testUnwrap, "UnwrapMetadataContext") + var cache *types.AuthzCache + testGet := func() { + cache = types.GetAuthzCache(ctx) } require.NotPanics(t, testGet, "GetAuthzCache") assert.NotNil(t, cache, "cache returned by GetAuthzCache") - assert.Empty(t, cache.AcceptableMap(), "cache acceptable map") -} - -func TestUsedSignersMap(t *testing.T) { - tests := []struct { - name string - actual keeper.UsedSignersMap - expected keeper.UsedSignersMap - isUsed []string - }{ - { - name: "NewUsedSignersMap", - actual: keeper.NewUsedSignersMap(), - expected: keeper.UsedSignersMap{}, - }, - { - name: "Use with two different addrs", - actual: keeper.NewUsedSignersMap().Use("addr1", "addr2"), - expected: keeper.UsedSignersMap{"addr1": true, "addr2": true}, - isUsed: []string{"addr1", "addr2"}, - }, - { - name: "Use with two same addrs", - actual: keeper.NewUsedSignersMap().Use("addr", "addr"), - expected: keeper.UsedSignersMap{"addr": true}, - isUsed: []string{"addr"}, - }, - { - name: "Use without any addrs", - actual: keeper.NewUsedSignersMap().Use(), - expected: keeper.UsedSignersMap{}, - }, - { - name: "Use twice different addrs", - actual: keeper.NewUsedSignersMap().Use("addr1").Use("addr2"), - expected: keeper.UsedSignersMap{"addr1": true, "addr2": true}, - isUsed: []string{"addr1", "addr2"}, - }, - { - name: "Use twice same addr", - actual: keeper.NewUsedSignersMap().Use("addr").Use("addr"), - expected: keeper.UsedSignersMap{"addr": true}, - isUsed: []string{"addr"}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expected, tc.actual) - }) - for _, addr := range tc.isUsed { - isUsed := tc.actual.IsUsed(addr) - assert.True(t, isUsed, "IsUsed(%q)", addr) - } - } -} - -func TestUsedSignersMap_AlsoUse(t *testing.T) { - tests := []struct { - name string - base keeper.UsedSignersMap - m2 keeper.UsedSignersMap - exp keeper.UsedSignersMap - }{ - { - name: "two different addrs", - base: keeper.NewUsedSignersMap().Use("addr1"), - m2: keeper.NewUsedSignersMap().Use("addr2"), - exp: keeper.UsedSignersMap{"addr1": true, "addr2": true}, - }, - { - name: "two same addrs", - base: keeper.NewUsedSignersMap().Use("addr"), - m2: keeper.NewUsedSignersMap().Use("addr"), - exp: keeper.UsedSignersMap{"addr": true}, - }, - { - name: "both empty", - base: keeper.NewUsedSignersMap(), - m2: keeper.NewUsedSignersMap(), - exp: keeper.UsedSignersMap{}, - }, - { - name: "base empty", - base: keeper.NewUsedSignersMap(), - m2: keeper.NewUsedSignersMap().Use("addr"), - exp: keeper.UsedSignersMap{"addr": true}, - }, - { - name: "m2 empty", - base: keeper.NewUsedSignersMap().Use("addr"), - m2: keeper.NewUsedSignersMap(), - exp: keeper.UsedSignersMap{"addr": true}, - }, - { - name: "m2 nil", - base: keeper.NewUsedSignersMap().Use("addr"), - m2: nil, - exp: keeper.UsedSignersMap{"addr": true}, - }, - { - name: "each have 3 with 1 common", - base: keeper.NewUsedSignersMap().Use("addr1", "addr2", "addr3"), - m2: keeper.NewUsedSignersMap().Use("addr3", "addr4", "addr5"), - exp: keeper.UsedSignersMap{ - "addr1": true, - "addr2": true, - "addr3": true, - "addr4": true, - "addr5": true, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual keeper.UsedSignersMap - testFunc := func() { - actual = tc.base.AlsoUse(tc.m2) - } - require.NotPanics(t, testFunc, "AlsoUse") - assert.Equal(t, tc.exp, actual, "AlsoUse return value") - assert.Equal(t, tc.exp, tc.base, "base after AlsoUse") - }) - } -} - -type testCaseFindMissing struct { - name string - required []string - toCheck []string - expected []string -} - -func testCasesForFindMissing() []testCaseFindMissing { - return []testCaseFindMissing{ - { - name: "nil required - nil toCheck - nil out", - required: nil, - toCheck: nil, - expected: nil, - }, - { - name: "empty required - nil toCheck - nil out", - required: []string{}, - toCheck: nil, - expected: nil, - }, - { - name: "nil required - empty toCheck - nil out", - required: nil, - toCheck: []string{}, - expected: nil, - }, - { - name: "empty required - empty toCheck - nil out", - required: []string{}, - toCheck: []string{}, - expected: nil, - }, - { - name: "nil required - 2 toCheck - nil out", - required: nil, - toCheck: []string{"one", "two"}, - expected: nil, - }, - { - name: "empty required - 2 toCheck - nil out", - required: []string{}, - toCheck: []string{"one", "two"}, - expected: nil, - }, - { - name: "1 required - is only toCheck - nil out", - required: []string{"one"}, - toCheck: []string{"one"}, - expected: nil, - }, - { - name: "1 required - is 1st of 2 toCheck - nil out", - required: []string{"one"}, - toCheck: []string{"one", "two"}, - expected: nil, - }, - { - name: "1 required - is 2nd of 2 toCheck - nil out", - required: []string{"one"}, - toCheck: []string{"two", "one"}, - expected: nil, - }, - { - name: "1 required - nil toCheck - required out", - required: []string{"one"}, - toCheck: nil, - expected: []string{"one"}, - }, - { - name: "1 required - empty toCheck - required out", - required: []string{"one"}, - toCheck: []string{}, - expected: []string{"one"}, - }, - { - name: "1 required - 1 other in toCheck - required out", - required: []string{"one"}, - toCheck: []string{"two"}, - expected: []string{"one"}, - }, - { - name: "1 required - 2 other in toCheck - required out", - required: []string{"one"}, - toCheck: []string{"two", "three"}, - expected: []string{"one"}, - }, - { - name: "2 required - both in toCheck - nil out", - required: []string{"one", "two"}, - toCheck: []string{"one", "two"}, - expected: nil, - }, - { - name: "2 required - reversed in toCheck - nil out", - required: []string{"one", "two"}, - toCheck: []string{"two", "one"}, - expected: nil, - }, - { - name: "2 required - only 1st in toCheck - 2nd out", - required: []string{"one", "two"}, - toCheck: []string{"one"}, - expected: []string{"two"}, - }, - { - name: "2 required - only 2nd in toCheck - 1st out", - required: []string{"one", "two"}, - toCheck: []string{"two"}, - expected: []string{"one"}, - }, - { - name: "2 required - 1st and other in toCheck - 2nd out", - required: []string{"one", "two"}, - toCheck: []string{"one", "other"}, - expected: []string{"two"}, - }, - { - name: "2 required - 2nd and other in toCheck - 1st out", - required: []string{"one", "two"}, - toCheck: []string{"two", "other"}, - expected: []string{"one"}, - }, - { - name: "2 required - nil toCheck - required out", - required: []string{"one", "two"}, - toCheck: nil, - expected: []string{"one", "two"}, - }, - { - name: "2 required - empty toCheck - required out", - required: []string{"one", "two"}, - toCheck: []string{}, - expected: []string{"one", "two"}, - }, - { - name: "2 required - neither in 1 toCheck - required out", - required: []string{"one", "two"}, - toCheck: []string{"neither"}, - expected: []string{"one", "two"}, - }, - { - name: "2 required - neither in 3 toCheck - required out", - required: []string{"one", "two"}, - toCheck: []string{"neither", "nor", "nothing"}, - expected: []string{"one", "two"}, - }, - { - name: "2 required - 1st not in 3 toCheck 2nd at 0 - 1st out", - required: []string{"one", "two"}, - toCheck: []string{"two", "nor", "nothing"}, - expected: []string{"one"}, - }, - { - name: "2 required - 1st not in 3 toCheck 2nd at 1 - 1st out", - required: []string{"one", "two"}, - toCheck: []string{"neither", "two", "nothing"}, - expected: []string{"one"}, - }, - { - name: "2 required - 1s5 not in 3 toCheck 2nd at 2 - 1st out", - required: []string{"one", "two"}, - toCheck: []string{"neither", "nor", "two"}, - expected: []string{"one"}, - }, - { - name: "2 required - 2nd not in 3 toCheck 1st at 0 - 2nd out", - required: []string{"one", "two"}, - toCheck: []string{"one", "nor", "nothing"}, - expected: []string{"two"}, - }, - { - name: "2 required - 2nd not in 3 toCheck 1st at 1 - 2nd out", - required: []string{"one", "two"}, - toCheck: []string{"neither", "one", "nothing"}, - expected: []string{"two"}, - }, - { - name: "2 required - 2nd not in 3 toCheck 1st at 2 - 2nd out", - required: []string{"one", "two"}, - toCheck: []string{"neither", "nor", "one"}, - expected: []string{"two"}, - }, - - { - name: "3 required - none in 5 toCheck - required out", - required: []string{"one", "two", "three"}, - toCheck: []string{"other1", "other2", "other3", "other4", "other5"}, - expected: []string{"one", "two", "three"}, - }, - { - name: "3 required - only 1st in 5 toCheck - 2nd 3rd out", - required: []string{"one", "two", "three"}, - toCheck: []string{"other1", "other2", "one", "other4", "other5"}, - expected: []string{"two", "three"}, - }, - { - name: "3 required - only 2nd in 5 toCheck - 1st 3rd out", - required: []string{"one", "two", "three"}, - toCheck: []string{"other1", "two", "other3", "other4", "other5"}, - expected: []string{"one", "three"}, - }, - { - name: "3 required - only 3rd in 5 toCheck - 1st 2nd out", - required: []string{"one", "two", "three"}, - toCheck: []string{"other1", "other2", "other3", "three", "other5"}, - expected: []string{"one", "two"}, - }, - { - name: "3 required - 1st 2nd in 5 toCheck - 3rd out", - required: []string{"one", "two", "three"}, - toCheck: []string{"other1", "two", "other3", "one", "other5"}, - expected: []string{"three"}, - }, - { - name: "3 required - 1st 3nd in 5 toCheck - 2nd out", - required: []string{"one", "two", "three"}, - toCheck: []string{"three", "other2", "other3", "other4", "one"}, - expected: []string{"two"}, - }, - { - name: "3 required - 2nd 3rd in 5 toCheck - 1st out", - required: []string{"one", "two", "three"}, - toCheck: []string{"other1", "other2", "two", "three", "other5"}, - expected: []string{"one"}, - }, - { - name: "3 required - all in 5 toCheck - nil out", - required: []string{"one", "two", "three"}, - toCheck: []string{"two", "other2", "one", "three", "other5"}, - expected: nil, - }, - { - name: "3 required with dup - all in toCheck - nil out", - required: []string{"one", "two", "one"}, - toCheck: []string{"one", "two"}, - expected: nil, - }, - { - name: "3 required with dup - dup not in toCheck - dups out", - required: []string{"one", "two", "one"}, - toCheck: []string{"two"}, - expected: []string{"one", "one"}, - }, - { - name: "3 required with dup - other not in toCheck - other out", - required: []string{"one", "two", "one"}, - toCheck: []string{"one"}, - expected: []string{"two"}, - }, - { - name: "3 required all dup - in toCheck - nil out", - required: []string{"one", "one", "one"}, - toCheck: []string{"one"}, - expected: nil, - }, - { - name: "3 required all dup - not in toCheck - all 3 out", - required: []string{"one", "one", "one"}, - toCheck: []string{"two"}, - expected: []string{"one", "one", "one"}, - }, - } -} - -func TestFindMissing(t *testing.T) { - for _, tc := range testCasesForFindMissing() { - t.Run(tc.name, func(t *testing.T) { - actual := keeper.FindMissing(tc.required, tc.toCheck) - assert.Equal(t, tc.expected, actual, "findMissing") - }) - } -} - -func TestFindMissingParties(t *testing.T) { - // pz is just a shorter way to define a []types.Party - pz := func(parties ...types.Party) []types.Party { - return parties - } - - pOne3Req := types.Party{Address: "one", Role: 3, Optional: false} - pOne3Opt := types.Party{Address: "one", Role: 3, Optional: true} - pOne4Req := types.Party{Address: "one", Role: 4, Optional: false} - pOne4Opt := types.Party{Address: "one", Role: 4, Optional: true} - pTwo3Req := types.Party{Address: "two", Role: 3, Optional: false} - pTwo3Opt := types.Party{Address: "two", Role: 3, Optional: true} - pTwo4Req := types.Party{Address: "two", Role: 4, Optional: false} - pTwo4Opt := types.Party{Address: "two", Role: 4, Optional: true} - - // Note: types.PartyType_PARTY_TYPE_INVESTOR = 3, types.PartyType_PARTY_TYPE_CUSTODIAN = 4 - - tests := []struct { - name string - required []types.Party - toCheck []types.Party - expected []types.Party - }{ - { - name: "nil nil", - required: nil, - toCheck: nil, - expected: nil, - }, - { - name: "empty nil", - required: pz(), - toCheck: nil, - expected: nil, - }, - { - name: "nil empty", - required: nil, - toCheck: pz(), - expected: nil, - }, - { - name: "empty empty", - required: pz(), - toCheck: pz(), - expected: nil, - }, - - { - name: "nil VS one3", - required: nil, - toCheck: pz(pOne3Req), - expected: nil, - }, - { - name: "empty VS one3", - required: pz(), - toCheck: pz(pOne3Req), - expected: nil, - }, - - { - name: "one3req VS one3req", - required: pz(pOne3Req), - toCheck: pz(pOne3Req), - expected: nil, - }, - { - name: "one3req VS one3opt", - required: pz(pOne3Req), - toCheck: pz(pOne3Opt), - expected: nil, - }, - { - name: "one3opt VS one3req", - required: pz(pOne3Opt), - toCheck: pz(pOne3Req), - expected: nil, - }, - { - name: "one3opt VS one3opt", - required: pz(pOne3Opt), - toCheck: pz(pOne3Opt), - expected: nil, - }, - - { - name: "one3 one4 two3 two4 req VS one4 one3 two4 two3 req", - required: pz(pOne3Req, pOne4Req, pTwo3Req, pTwo4Req), - toCheck: pz(pOne4Req, pOne3Req, pTwo4Req, pTwo3Req), - expected: nil, - }, - { - name: "one3 one4 two3 two4 req VS one4 one3 two4 two3 opt", - required: pz(pOne3Req, pOne4Req, pTwo3Req, pTwo4Req), - toCheck: pz(pOne4Opt, pOne3Opt, pTwo4Opt, pTwo3Opt), - expected: nil, - }, - { - name: "one3 one4 two3 two4 opt vs one4 one3 two4 two3 req", - required: pz(pOne3Opt, pOne4Opt, pTwo3Opt, pTwo4Opt), - toCheck: pz(pOne4Req, pOne3Req, pTwo4Req, pTwo3Req), - expected: nil, - }, - { - name: "one3 one4 two3 two4 opt vs one4 one3 two4 two3 opt", - required: pz(pOne3Opt, pOne4Opt, pTwo3Opt, pTwo4Opt), - toCheck: pz(pOne4Opt, pOne3Opt, pTwo4Opt, pTwo3Opt), - expected: nil, - }, - - { - name: "one3 two4 VS nil", - required: pz(pOne3Opt, pTwo4Req), - toCheck: nil, - expected: pz(pOne3Opt, pTwo4Req), - }, - { - name: "one3 two4 VS empty", - required: pz(pOne3Opt, pTwo4Req), - toCheck: pz(), - expected: pz(pOne3Opt, pTwo4Req), - }, - { - name: "one3 two4 VS one3", - required: pz(pOne3Opt, pTwo4Req), - toCheck: pz(pOne3Req), - expected: pz(pTwo4Req), - }, - { - name: "one3 two4 VS one4", - required: pz(pOne3Opt, pTwo4Req), - toCheck: pz(pOne4Opt), - expected: pz(pOne3Opt, pTwo4Req), - }, - { - name: "one3 two4 VS two3", - required: pz(pOne3Opt, pTwo4Req), - toCheck: pz(pTwo3Opt), - expected: pz(pOne3Opt, pTwo4Req), - }, - { - name: "one3 two4 VS two4", - required: pz(pOne3Opt, pTwo4Req), - toCheck: pz(pTwo4Opt), - expected: pz(pOne3Opt), - }, - - { - name: "one3req two4opt VS two4req one3opt", - required: pz(pOne3Req, pTwo4Opt), - toCheck: pz(pTwo4Req, pOne3Opt), - expected: nil, - }, - { - name: "one3opt two4req VS two4opt one3req", - required: pz(pOne3Opt, pTwo4Req), - toCheck: pz(pTwo4Opt, pOne3Req), - expected: nil, - }, - - { - name: "one3opt VS all others req", - required: pz(pOne3Opt), - toCheck: pz(pOne3Req, pOne4Req, pTwo3Req, pTwo4Req), - expected: nil, - }, - { - name: "one3req VS all others opt", - required: pz(pOne3Req), - toCheck: pz(pOne3Opt, pOne4Opt, pTwo3Opt, pTwo4Opt), - expected: nil, - }, - { - name: "all req VS two3Opt", - required: pz(pOne4Req, pTwo3Req, pOne3Req, pTwo4Req), - toCheck: pz(pTwo3Opt), - expected: pz(pOne4Req, pOne3Req, pTwo4Req), - }, - { - name: "all opt VS two3Req", - required: pz(pOne4Opt, pOne3Opt, pTwo3Opt, pTwo4Opt), - toCheck: pz(pTwo3Req), - expected: pz(pOne4Opt, pOne3Opt, pTwo4Opt), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := keeper.FindMissingParties(tc.required, tc.toCheck) - assert.Equal(t, tc.expected, actual, "findMissingParties") - }) - } -} - -func TestFindMissingComp(t *testing.T) { - t.Run("equals equals", func(t *testing.T) { - comp := func(r, c string) bool { - return r == c - } - for _, tc := range testCasesForFindMissing() { - t.Run(tc.name, func(t *testing.T) { - actual := keeper.FindMissingComp(tc.required, tc.toCheck, comp) - assert.Equal(t, tc.expected, actual, "findMissingComp") - }) - } - }) - - t.Run("is same as same types", func(t *testing.T) { - comp := func(r, c stringSame) bool { - return r.IsSameAs(c) - } - for _, tc := range testCasesForFindMissing() { - t.Run(tc.name, func(t *testing.T) { - required := newStringSames(tc.required) - toCheck := newStringSames(tc.toCheck) - expected := newStringSames(tc.expected) - actual := keeper.FindMissingComp(required, toCheck, comp) - assert.Equal(t, expected, actual, "findMissingComp") - }) - } - }) - - t.Run("is same as different types", func(t *testing.T) { - comp := func(r stringSameR, c stringSameC) bool { - return r.IsSameAs(c) - } - for _, tc := range testCasesForFindMissing() { - t.Run(tc.name, func(t *testing.T) { - required := newStringSameRs(tc.required) - toCheck := newStringSameCs(tc.toCheck) - expected := newStringSameRs(tc.expected) - actual := keeper.FindMissingComp(required, toCheck, comp) - assert.Equal(t, expected, actual, "findMissingComp") - }) - } - }) - - t.Run("string lengths", func(t *testing.T) { - comp := func(r string, c int) bool { - return len(r) == c - } - req := []string{"a", "bb", "ccc", "dddd", "eeeee"} - checks := []struct { - name string - toCheck []int - expected []string - }{ - {name: "all there", toCheck: []int{1, 2, 3, 4, 5}, expected: nil}, - {name: "missing len 1", toCheck: []int{2, 3, 4, 5}, expected: []string{"a"}}, - {name: "missing len 2", toCheck: []int{1, 3, 4, 5}, expected: []string{"bb"}}, - {name: "missing len 3", toCheck: []int{1, 2, 4, 5}, expected: []string{"ccc"}}, - {name: "missing len 4", toCheck: []int{1, 2, 3, 5}, expected: []string{"dddd"}}, - {name: "missing len 5", toCheck: []int{1, 2, 3, 4}, expected: []string{"eeeee"}}, - {name: "none there", toCheck: []int{0, 6}, expected: req}, - } - for _, tc := range checks { - t.Run(tc.name, func(t *testing.T) { - actual := keeper.FindMissingComp(req, tc.toCheck, comp) - assert.Equal(t, tc.expected, actual, "findMissingComp") - }) - } - }) - - t.Run("div two", func(t *testing.T) { - comp := func(r int, c int) bool { - return r/2 == c - } - req := []int{1, 2, 3, 4, 5} - checks := []struct { - name string - toCheck []int - expected []int - }{ - {name: "all there", toCheck: []int{0, 1, 2}, expected: nil}, - {name: "missing 0", toCheck: []int{1, 2}, expected: []int{1}}, - {name: "missing 1", toCheck: []int{0, 2}, expected: []int{2, 3}}, - {name: "missing 2", toCheck: []int{0, 1}, expected: []int{4, 5}}, - {name: "none there", toCheck: []int{-1, 3}, expected: req}, - } - for _, tc := range checks { - t.Run(tc.name, func(t *testing.T) { - actual := keeper.FindMissingComp(req, tc.toCheck, comp) - assert.Equal(t, tc.expected, actual, "findMissingComp") - }) - } - }) - - t.Run("all true", func(t *testing.T) { - comp := func(r, c string) bool { - return true - } - for _, tc := range testCasesForFindMissing() { - t.Run(tc.name, func(t *testing.T) { - var expected []string - // required entries are only marked as found after being compared to something. - // So if there's nothing in the toCheck list, all the required will be returned. - // But if tc.required is an empty slice, we still expect to get nil back, so we don't - // set expected = tc.required in that case. - if len(tc.toCheck) == 0 && len(tc.required) > 0 { - expected = tc.required - } - actual := keeper.FindMissingComp(tc.required, tc.toCheck, comp) - assert.Equal(t, expected, actual, "findMissingComp comp always returns true") - }) - } - }) - - t.Run("all false", func(t *testing.T) { - comp := func(r, c string) bool { - return false - } - for _, tc := range testCasesForFindMissing() { - t.Run(tc.name, func(t *testing.T) { - // If tc.required is nil, or an empty slice, we expect nil, otherwise, we always expect tc.required back. - var expected []string - if len(tc.required) > 0 { - expected = tc.required - } - actual := keeper.FindMissingComp(tc.required, tc.toCheck, comp) - assert.Equal(t, expected, actual, "findMissingComp comp always returns false") - }) - } - }) -} - -func TestPluralEnding(t *testing.T) { - tests := []struct { - i int - exp string - }{ - {i: 0, exp: "s"}, - {i: 1, exp: ""}, - {i: -1, exp: "s"}, - {i: 2, exp: "s"}, - {i: 3, exp: "s"}, - {i: 5, exp: "s"}, - {i: 50, exp: "s"}, - {i: -100, exp: "s"}, - } - - for _, tc := range tests { - t.Run(fmt.Sprintf("%d", tc.i), func(t *testing.T) { - actual := keeper.PluralEnding(tc.i) - assert.Equal(t, tc.exp, actual, "pluralEnding(%d)", tc.i) - }) - } + assert.Empty(t, cache.GetAcceptableMap(), "cache acceptable map") } func TestSafeBech32ToAccAddresses(t *testing.T) { diff --git a/x/metadata/keeper/specification.go b/x/metadata/keeper/specification.go index 315e4e54e4..6e284ee115 100644 --- a/x/metadata/keeper/specification.go +++ b/x/metadata/keeper/specification.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" + "github.com/provenance-io/provenance/internal/provutils" "github.com/provenance-io/provenance/x/metadata/types" ) @@ -284,7 +285,7 @@ func getMissingContractSpecIndexValues(required, found *contractSpecIndexValues) return *required } rv.SpecificationID = required.SpecificationID - rv.OwnerAddresses = findMissing(required.OwnerAddresses, found.OwnerAddresses) + rv.OwnerAddresses = provutils.FindMissing(required.OwnerAddresses, found.OwnerAddresses) return rv } @@ -512,7 +513,7 @@ func getMissingScopeSpecIndexValues(required, found *scopeSpecIndexValues) scope return *required } rv.SpecificationID = required.SpecificationID - rv.OwnerAddresses = findMissing(required.OwnerAddresses, found.OwnerAddresses) + rv.OwnerAddresses = provutils.FindMissing(required.OwnerAddresses, found.OwnerAddresses) rv.ContractSpecIDs = FindMissingMdAddr(required.ContractSpecIDs, found.ContractSpecIDs) return rv } diff --git a/x/metadata/keeper/specification_test.go b/x/metadata/keeper/specification_test.go index ef2bed54d2..a3ade83486 100644 --- a/x/metadata/keeper/specification_test.go +++ b/x/metadata/keeper/specification_test.go @@ -72,7 +72,7 @@ func (s *SpecKeeperTestSuite) SetupTest() { } func (s *SpecKeeperTestSuite) FreshCtx() sdk.Context { - return keeper.AddAuthzCacheToContext(s.app.BaseApp.NewContext(false)) + return FreshCtx(s.app) } func containsMetadataAddress(arr []types.MetadataAddress, newVal types.MetadataAddress) bool { diff --git a/x/metadata/module.go b/x/metadata/module.go index d4ed3a2184..eeefd3aae3 100644 --- a/x/metadata/module.go +++ b/x/metadata/module.go @@ -27,15 +27,6 @@ import ( "github.com/provenance-io/provenance/x/metadata/types" ) -// ModuleName is the public name of this module -const ModuleName = types.ModuleName - -// RouterKey is the public routing key of this module -const RouterKey = types.RouterKey - -// StoreKey is the public key-value store key of this module. -const StoreKey = types.StoreKey - // type check to ensure the interface is properly implemented var ( _ module.AppModuleBasic = (*AppModule)(nil) @@ -129,6 +120,11 @@ func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) { func (am AppModule) RegisterServices(cfg module.Configurator) { types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) types.RegisterQueryServer(cfg.QueryServer(), am.keeper) + + m := keeper.NewMigrator(am.keeper) + if err := cfg.RegisterMigration(types.ModuleName, 3, m.Migrate3To4); err != nil { + panic(fmt.Sprintf("failed to register x/metadata migration from version 3 to 4: %v", err)) + } } // InitGenesis performs genesis initialization for the metadata module. It returns no validator updates. @@ -171,4 +167,4 @@ func (am AppModule) WeightedOperations(_ module.SimulationState) []simtypes.Weig } // ConsensusVersion implements AppModule/ConsensusVersion. -func (AppModule) ConsensusVersion() uint64 { return 3 } +func (AppModule) ConsensusVersion() uint64 { return 4 } diff --git a/x/metadata/spec/01_concepts.md b/x/metadata/spec/01_concepts.md index 0e1796db56..40904f7475 100644 --- a/x/metadata/spec/01_concepts.md +++ b/x/metadata/spec/01_concepts.md @@ -33,7 +33,7 @@ See [Specifications](02_state.md#specifications) for details. ## Metadata Addresses Entries and Specifications must each have a unique metadata address. -These addresses are byte arrays that are commonly referered to as "ids". +These addresses are byte arrays that are commonly referred to as "ids". As strings, they should be represented using the bech32 address format. The addresses for the different messages have specific formats that help facilitate grouping and indexing. All addresses start with a single byte that identifies the type, and are followed by 16 bytes commonly called a UUID. @@ -105,6 +105,7 @@ The following are requirements related to smart contract usage of the `x/metadat * When a smart contract signs a message, it MUST be first or have only smart-contract signers before it, and SHOULD include the invoker address(es) after. * When a smart contract is a signer, it must either be a party/owner, or have authorizations (via `x/authz`) from all signers after it. * If a smart contract is a signer, but not a party, it cannot be the only signer, and cannot be the last signer. +* A smart contract cannot be used to change the value owner of a scope unless the smart contract is the value owner itself. ### With Party Rollup Required diff --git a/x/metadata/spec/02_state.md b/x/metadata/spec/02_state.md index c946b10d65..81c6289b15 100644 --- a/x/metadata/spec/02_state.md +++ b/x/metadata/spec/02_state.md @@ -43,7 +43,7 @@ Byte Array Length: `17` #### Scope Values -+++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/scope.proto#L70-L90 ++++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/scope.proto#L70-L102 ```protobuf // Scope defines a root reference for a collection of records owned by one or more parties. @@ -59,8 +59,20 @@ message Scope { repeated Party owners = 3 [(gogoproto.nullable) = false]; // Addresses in this list are authorized to receive off-chain data associated with this scope. repeated string data_access = 4; - // An address that controls the value associated with this scope. Standard blockchain accounts and marker accounts - // are supported for this value. This attribute may only be changed by the entity indicated once it is set. + // The address that controls the value associated with this scope. + // + // The value owner is actually tracked by the bank module using a coin with the denom "nft/". + // The value owner can be changed using WriteScope or anything that transfers funds, e.g. MsgSend. + // + // During WriteScope: + // - If this field is empty, it indicates that there should not be a change to the value owner. + // I.e. Once a scope has a value owner, it will always have one (until it's deleted). + // - If this field has a value, the existing value owner will be looked up, and + // - If there's already an existing value owner, they must be a signer, + // and the coin will be transferred to the new value owner. + // - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + // If the scope already exists, the owners must be signers (just like changing other fields). + // If it's a new scope, there's no special signer limitations related to the value owner. string value_owner_address = 5; // Whether all parties in this scope and its sessions must be present in this scope's owners field. // This also enables use of optional=true scope owners and session parties. @@ -68,6 +80,15 @@ message Scope { } ``` +Before a scope is stored in state, the `value_owner_address` is cleared out (set to an empty string). +The scope is then protobuf encoded, and those bytes are the value stored in state. + +#### Scope Value Owners + +The `value_owner_address` is tracked using the `x/bank` module. When a scope first gets a value owner (either upon scope +creation, or later with an update), a single coin with the denom `nft/` is minted and placed in the value +owner's account. That coin can be transferred or traded the same ways as any other on-chain funds, e.g. via `MsgSend`. + #### Scope Indexes Scopes by owner: @@ -81,11 +102,6 @@ Scopes by Scope Specification: * Part 1: All bytes of the scope specification key * Part 2: All bytes of the scope key -Scopes by value owner: -* Type byte: `0x18` -* Part 1: The value owner address (length byte then value bytes) -* Part 2: All bytes of the scope key - ### Sessions @@ -113,7 +129,7 @@ Byte Array Length: `33` #### Session Values -+++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/scope.proto#L92-L111 ++++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/scope.proto#L104-L123 ```protobuf // Session defines an execution context against a specific specification instance. @@ -170,7 +186,7 @@ Byte Array Length: `33` #### Record Values -+++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/scope.proto#L113-L130 ++++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/scope.proto#L125-L142 ```protobuf // A record (of fact) is attached to a session or each consideration output from a contract diff --git a/x/metadata/spec/03_messages.md b/x/metadata/spec/03_messages.md index c102a0656f..816d2b785b 100644 --- a/x/metadata/spec/03_messages.md +++ b/x/metadata/spec/03_messages.md @@ -51,13 +51,17 @@ Scopes are identified using their `scope_id`. +++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/tx.proto#L93-L119 The `scope_uuid` field is optional. -It should be a uuid formated as a string using the standard UUID format. +It should be a uuid formatted as a string using the standard UUID format. If supplied, it will be used to generate the appropriate scope id for use in the `scope.scope_id` field. The `spec_uuid` field is optional. -It should be a uuid formated as a string using the standard UUID format. +It should be a uuid formatted as a string using the standard UUID format. If supplied, it will be used to generate the appropriate scope specification id for use in the `scope.specification_id` field. +An empty `scope.value_owner_address` indicates that there is no change to the scope's value owner. I.e. Once a scope has +a value owner, it will always have a value owner (until the scope is deleted); it cannot be changed to an empty string. + + #### Response +++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/tx.proto#L121-L125 @@ -227,7 +231,7 @@ The `session_id_components` field is optional. If supplied, it will be used to generate the appropriate session id for use in the `session.session_id` field. The `spec_uuid` field is optional. -It should be a uuid formated as a string using the standard UUID format. +It should be a uuid formatted as a string using the standard UUID format. If supplied, it will be used to generate the appropriate contract specification id for use in the `session.specification_id` field. #### Response @@ -265,7 +269,7 @@ The `session_id_components` field is optional. If supplied, it will be used to generate the appropriate session id for use in the `record.session_id` field. The `contract_spec_uuid` field is optional. -It should be a uuid formated as a string using the standard UUID format. +It should be a uuid formatted as a string using the standard UUID format. If supplied, it will be used with `record.name` to generate the appropriate record specification id for use in the `record.specification_id` field. #### Response @@ -300,7 +304,7 @@ This service message is expected to fail if: * The `inputs` list does not contain one or more inputs defined in the record specification. * An entry in `inputs` has a `type_name` different from its input specification. * An entry in `inputs` has a `source` type that doesn't match the input specification. -* An entry in `inputs` has a `source` value that doesn't match the intput specification. +* An entry in `inputs` has a `source` value that doesn't match the input specification. * The record specification has a result type of `record` but there isn't exactly one entry in `outputs`. * The record specification has a result type of `record_list` but the `outputs` list is empty. * The `signers` do not have permission to write the record. @@ -340,7 +344,7 @@ Scope specifications are identified using their `specification_id`. +++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/tx.proto#L345-L363 The `spec_uuid` field is optional. -It should be a uuid formated as a string using the standard UUID format. +It should be a uuid formatted as a string using the standard UUID format. If supplied, it will be used to generate the appropriate scope specification id for use in the `specification.specification_id` field. #### Response @@ -393,7 +397,7 @@ Contract specifications are identified using their `specification_id`. +++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/tx.proto#L385-L403 The `spec_uuid` field is optional. -It should be a uuid formated as a string using the standard UUID format. +It should be a uuid formatted as a string using the standard UUID format. If supplied, it will be used to generate the appropriate contract specification id for use in the `specification.specification_id` field. #### Response @@ -496,7 +500,7 @@ Record specifications are identified using their `specification_id`. +++ https://github.com/provenance-io/provenance/blob/v1.19.0/proto/provenance/metadata/v1/tx.proto#L461-L479 The `contract_spec_uuid` field is optional. -It should be a uuid formated as a string using the standard UUID format. +It should be a uuid formatted as a string using the standard UUID format. If supplied, it will be used with the `specification.name` to generate the appropriate record specification id for use in the `specification.specification_id` field. #### Response diff --git a/x/metadata/types/address.go b/x/metadata/types/address.go index eb8d23ba66..8cb95fe808 100644 --- a/x/metadata/types/address.go +++ b/x/metadata/types/address.go @@ -31,6 +31,9 @@ const ( PrefixContractSpecification = "contractspec" // PrefixRecordSpecification is the address human readable prefix used with bech32 encoding of RecordSpecification IDs PrefixRecordSpecification = "recspec" + + // DenomPrefix is the string prepended to a metadata address to create the denom for that metadata object. + DenomPrefix = "nft/" ) var ( @@ -90,6 +93,33 @@ func VerifyMetadataAddressFormat(bz []byte) (string, error) { return hrp, nil } +// getNameForHRP returns the more formal name used for each metadata hrp. +// E.g. if the hrp is PrefixRecordSpecification (i.e. "recspec"), this will return "record specification". +func getNameForHRP(hrp string) string { + switch hrp { + case PrefixScope, PrefixSession, PrefixRecord: + return hrp + case PrefixScopeSpecification, PrefixContractSpecification: + return strings.TrimSuffix(hrp, "spec") + " specification" + case PrefixRecordSpecification: + return "record specification" + } + return fmt.Sprintf("<%q>", hrp) +} + +// VerifyMetadataAddressHasType makes sure that the provided ma is a valid MetadataAddress +// and that it has the provided type (e.g. PrefixScope or PrefixContractSpecification, etc.). +func VerifyMetadataAddressHasType(ma MetadataAddress, expHRP string) error { + hrp, err := VerifyMetadataAddressFormat(ma) + if err != nil { + return fmt.Errorf("invalid %s metadata address %#v: %w", getNameForHRP(expHRP), ma, err) + } + if hrp != expHRP { + return fmt.Errorf("invalid %s id %q: wrong type", getNameForHRP(expHRP), ma) + } + return nil +} + // ConvertHashToAddress constructs a MetadataAddress using the provided type code and the raw bytes of the // base64 decoded hash, limited appropriately by the desired typeCode. // The resulting Address is not guaranteed to contain valid UUIDS or name hashes. @@ -173,6 +203,19 @@ func MetadataAddressFromBech32(address string) (addr MetadataAddress, err error) return bz, err } +// MetadataAddressFromDenom gets the MetadataAddress that the provided denom is for. +func MetadataAddressFromDenom(denom string) (MetadataAddress, error) { + id := strings.TrimPrefix(denom, DenomPrefix) + if id == denom { + return nil, fmt.Errorf("denom %q is not a MetadataAddress denom", denom) + } + rv, err := MetadataAddressFromBech32(id) + if err != nil { + return nil, fmt.Errorf("invalid metadata address in denom %q: %w", denom, err) + } + return rv, nil +} + // ScopeMetadataAddress creates a MetadataAddress instance for the given scope by its uuid func ScopeMetadataAddress(scopeUUID uuid.UUID) MetadataAddress { bz, err := scopeUUID.MarshalBinary() @@ -353,7 +396,9 @@ func (ma MetadataAddress) String() string { hrp, err := VerifyMetadataAddressFormat(ma) if err != nil { - panic(err) + // Be careful changing this. The %#v path in MetadataAddress.Format does NOT call this String method, + // but %v, %q and %s do. So there would be infinite recursion with some other formatter directives. + return fmt.Sprintf("%#v", ma) } bech32Addr, err := bech32.ConvertAndEncode(hrp, ma.Bytes()) @@ -644,7 +689,8 @@ func (ma MetadataAddress) Format(s fmt.State, verb rune) { out = fmt.Sprintf(fmt.FormatString(s, verb), ma.String()) } default: - // The 'p' (pointer) and 'T' (type) verbs are never processed using this Format method. + // The other verbs (e.g. c b o O d x X U) should behave just like if this were a []byte. + // Note that the 'p' (pointer) and 'T' (type) verbs are never processed using this Format method. // That's how %T returns the correct type even though it would actually return "[]byte" if run through this. out = fmt.Sprintf(fmt.FormatString(s, verb), []byte(ma)) } @@ -704,6 +750,16 @@ func (ma MetadataAddress) isTypeOneOf(options ...[]byte) bool { return false } +// ValidateIsScopeAddress returns an error if this isn't a valid MetadataAddress or if it's not a scope. +func (ma MetadataAddress) ValidateIsScopeAddress() error { + return VerifyMetadataAddressHasType(ma, PrefixScope) +} + +// ValidateIsScopeSpecificationAddress returns an error if this isn't a valid MetadataAddress or if it's not a scope specification. +func (ma MetadataAddress) ValidateIsScopeSpecificationAddress() error { + return VerifyMetadataAddressHasType(ma, PrefixScopeSpecification) +} + // MetadataAddressDetails contains a breakdown of the components in a MetadataAddress. type MetadataAddressDetails struct { // Address is the full MetadataAddress in question. @@ -739,6 +795,7 @@ type MetadataAddressDetails struct { ParentAddress MetadataAddress } +// GetDetails breaks this MetadataAddress down into its various components, and returns all the details. func (ma MetadataAddress) GetDetails() MetadataAddressDetails { // Copying this MetadataAddress to prevent weird behavior. addr := make(MetadataAddress, len(ma)) @@ -801,3 +858,171 @@ func (ma MetadataAddress) GetDetails() MetadataAddressDetails { } return retval } + +// Denom gets the denom string for this MetadataAddress. +func (ma MetadataAddress) Denom() string { + return DenomPrefix + ma.String() +} + +// Coin creates the singular coin that represents this MetadataAddress. +func (ma MetadataAddress) Coin() sdk.Coin { + return sdk.NewInt64Coin(ma.Denom(), 1) +} + +// Coins creates a Coins with just the singular coin that represents this MetadataAddress. +func (ma MetadataAddress) Coins() sdk.Coins { + return sdk.Coins{sdk.NewInt64Coin(ma.Denom(), 1)} +} + +// AccMDLink associates an account address with a metadata address. +type AccMDLink struct { + AccAddr sdk.AccAddress + MDAddr MetadataAddress +} + +// NewAccMDLink creates a new association between an account address and a metadata address. +func NewAccMDLink(accAddr sdk.AccAddress, mdAddr MetadataAddress) *AccMDLink { + return &AccMDLink{ + AccAddr: accAddr, + MDAddr: mdAddr, + } +} + +const ( + // nilStr is a string indicating that something is nil. + nilStr = "" + // emptyStr is a string indicating that something is empty. + emptyStr = "" +) + +// String returns a string representation of this AccMDLink in the format : using bech32 strings. +func (l *AccMDLink) String() string { + if l == nil { + return nilStr + } + return accStr(l.AccAddr) + ":" + mdStr(l.MDAddr) +} + +// accStr returns a string representation of an AccAddress, either bech32 or a string indicating nil or empty. +func accStr(acc sdk.AccAddress) string { + if len(acc) > 0 { + return acc.String() + } + if acc == nil { + return nilStr + } + return emptyStr +} + +// accStr returns a string representation of a MetadataAddress, either bech32 or a string indicating nil or empty. +func mdStr(md MetadataAddress) string { + if len(md) > 0 { + return md.String() + } + if md == nil { + return nilStr + } + return emptyStr +} + +// AccMDLinks is a slice of AccMDLink. +type AccMDLinks []*AccMDLink + +// String returns a string representation of this AccMDLinks. +func (a AccMDLinks) String() string { + if len(a) > 0 { + strs := make([]string, len(a)) + for i, link := range a { + strs[i] = link.String() + } + return "[" + strings.Join(strs, ", ") + "]" + } + if a == nil { + return nilStr + } + return emptyStr +} + +// ValidateForScopes returns an error in the following cases: +// - An entry is nil. +// - An entry does not have an AccAddr. +// - An entry does not have a MDAddr. +// - An MDAddr is not a valid scope id. +// - Any MDAddr appears more than once. +func (a AccMDLinks) ValidateForScopes() error { + if len(a) == 0 { + return nil + } + + seenMDAddrs := make(map[string]int8) + for _, link := range a { + if link == nil { + return errors.New("nil entry not allowed") + } + + key := string(link.MDAddr) + switch seenMDAddrs[key] { + case 0: + seenMDAddrs[key] = 1 + if err := link.MDAddr.ValidateIsScopeAddress(); err != nil { + return err + } + case 1: + seenMDAddrs[key] = 2 + return fmt.Errorf("duplicate metadata address %q not allowed", link.MDAddr) + } + + if len(link.AccAddr) == 0 { + return fmt.Errorf("no account address associated with metadata address %q", link.MDAddr) + } + } + + return nil +} + +// GetAccAddrs returns a list of all AccAddr values from this AccMDLinks. +// Each entry will appear only once and in the order it first appears in this list. +// Empty and nil entries are ignored. +func (a AccMDLinks) GetAccAddrs() []sdk.AccAddress { + seen := make(map[string]bool) + var rv []sdk.AccAddress + for _, link := range a { + if link == nil || len(link.AccAddr) == 0 { + continue + } + if !seen[string(link.AccAddr)] { + seen[string(link.AccAddr)] = true + rv = append(rv, link.AccAddr) + } + } + return rv +} + +// GetPrimaryUUIDs extracts the primary UUID out of each MDAddr and converts each to a string. +// If the MDAddr is nil, empty, or invalid, it's corresponding result will be "". +func (a AccMDLinks) GetPrimaryUUIDs() []string { + if a == nil { + return nil + } + rv := make([]string, len(a)) + for i, link := range a { + if link != nil && len(link.MDAddr) > 0 { + id, err := link.MDAddr.PrimaryUUID() + if err == nil { + rv[i] = id.String() + } + } + } + return rv +} + +// GetMDAddrsForAccAddr returns all of the MDAddrs associated with the provided AccAddr. +func (a AccMDLinks) GetMDAddrsForAccAddr(addr string) []MetadataAddress { + var rv []MetadataAddress + for _, link := range a { + if link != nil && addr == link.AccAddr.String() { + rv = append(rv, link.MDAddr) + } + } + return rv +} diff --git a/x/metadata/types/address_test.go b/x/metadata/types/address_test.go index 5f0631c6ea..92d66f0bbc 100644 --- a/x/metadata/types/address_test.go +++ b/x/metadata/types/address_test.go @@ -1,12 +1,14 @@ package types import ( + "bytes" "crypto/sha256" "crypto/sha512" "encoding/base64" "encoding/hex" "errors" "fmt" + "strings" "testing" "github.com/google/uuid" @@ -14,8 +16,13 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/bech32" + + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/testutil/testlog" ) type AddressTestSuite struct { @@ -48,6 +55,30 @@ func TestAddressTestSuite(t *testing.T) { suite.Run(t, new(AddressTestSuite)) } +func mapToStrings[S ~[]E, E fmt.Stringer](vals S) []string { + if vals == nil { + return nil + } + rv := make([]string, len(vals)) + for i, v := range vals { + rv[i] = v.String() + } + return rv +} + +// subSet creates a new slice containing the entries of vals that have the given indexes. +// The resulting entries will be in the order that the indexes are provided. +func subSet[S ~[]E, E any](vals S, indexes ...int) S { + if len(indexes) == 0 { + return nil + } + rv := make(S, 0, len(indexes)) + for _, i := range indexes { + rv = append(rv, vals[i]) + } + return rv +} + func (s *AddressTestSuite) requireBech32String(typeCode []byte, data []byte) string { hrp, err := MetadataAddress{typeCode[0]}.Prefix() s.Require().NoError(err, "getPrefix error") @@ -420,6 +451,148 @@ func (s *AddressTestSuite) TestVerifyMetadataAddressFormat() { } } +func (s *AddressTestSuite) TestGetNameForHRP() { + tests := []struct { + hrp string + exp string + }{ + {hrp: PrefixScope, exp: "scope"}, + {hrp: PrefixSession, exp: "session"}, + {hrp: PrefixRecord, exp: "record"}, + {hrp: PrefixScopeSpecification, exp: "scope specification"}, + {hrp: PrefixContractSpecification, exp: "contract specification"}, + {hrp: PrefixRecordSpecification, exp: "record specification"}, + {hrp: "", exp: `<"">`}, + {hrp: "unknown", exp: `<"unknown">`}, + {hrp: `I might be "evil"`, exp: `<"I might be \"evil\"">`}, + {hrp: PrefixScope + "1", exp: `<"scope1">`}, + {hrp: PrefixScope + "spe", exp: `<"scopespe">`}, + {hrp: PrefixScope + "spec ", exp: `<"scopespec ">`}, + {hrp: PrefixScope + "specification", exp: `<"scopespecification">`}, + {hrp: PrefixScope[1:], exp: `<"cope">`}, + {hrp: PrefixScope[:len(PrefixScope)-1], exp: `<"scop">`}, + {hrp: PrefixRecord + "spec", exp: `<"recordspec">`}, + {hrp: PrefixRecord + "specification", exp: `<"recordspecification">`}, + } + + for _, tc := range tests { + name := tc.hrp + if len(name) == 0 { + name = "(empty)" + } + s.Run(name, func() { + var act string + testFunc := func() { + act = getNameForHRP(tc.hrp) + } + s.Require().NotPanics(testFunc, "getNameForHRP(%q)", tc.hrp) + s.Assert().Equal(tc.exp, act, "result from getNameForHRP(%q)", tc.hrp) + }) + } +} + +func (s *AddressTestSuite) TestVerifyMetadataAddressHasType() { + newUUID := func(i string) uuid.UUID { + id := strings.ReplaceAll("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "x", i) + rv, err := uuid.Parse(id) + s.Require().NoError(err, "uuid.Parse(%q)", id) + return rv + } + type testCase struct { + name string + ma MetadataAddress + hrp string + expErr string + } + + tests := []testCase{ + { + name: "nil", + ma: nil, + hrp: "whatever", + expErr: "invalid <\"whatever\"> metadata address MetadataAddress(nil): address is empty", + }, + { + name: "empty", + ma: MetadataAddress{}, + hrp: "thingy", + expErr: "invalid <\"thingy\"> metadata address MetadataAddress{}: address is empty", + }, + { + name: "invalid scope id", + ma: MetadataAddress{ScopeKeyPrefix[0], 0x1, 0x2}, + hrp: PrefixScope, + expErr: "invalid scope metadata address MetadataAddress{0x0, 0x1, 0x2}: incorrect address length (expected: 17, actual: 3)", + }, + { + name: "invalid session id", + ma: MetadataAddress{SessionKeyPrefix[0], 0x3, 0x4}, + hrp: PrefixSession, + expErr: "invalid session metadata address MetadataAddress{0x1, 0x3, 0x4}: incorrect address length (expected: 33, actual: 3)", + }, + { + name: "invalid record id", + ma: MetadataAddress{RecordKeyPrefix[0], 0x5, 0x6}, + hrp: PrefixRecord, + expErr: "invalid record metadata address MetadataAddress{0x2, 0x5, 0x6}: incorrect address length (expected: 33, actual: 3)", + }, + { + name: "invalid scope spec id", + ma: MetadataAddress{ScopeSpecificationKeyPrefix[0], 0x7, 0x8}, + hrp: PrefixScopeSpecification, + expErr: "invalid scope specification metadata address MetadataAddress{0x4, 0x7, 0x8}: incorrect address length (expected: 17, actual: 3)", + }, + { + name: "invalid contract spec id", + ma: MetadataAddress{ContractSpecificationKeyPrefix[0], 0x9, 0xa}, + hrp: PrefixContractSpecification, + expErr: "invalid contract specification metadata address MetadataAddress{0x3, 0x9, 0xa}: incorrect address length (expected: 17, actual: 3)", + }, + { + name: "invalid record spec id", + ma: MetadataAddress{RecordSpecificationKeyPrefix[0], 0xb, 0xc}, + hrp: PrefixRecordSpecification, + expErr: "invalid record specification metadata address MetadataAddress{0x5, 0xb, 0xc}: incorrect address length (expected: 33, actual: 3)", + }, + } + + validCases := []struct { + hrp string + ma MetadataAddress + }{ + {hrp: PrefixScope, ma: ScopeMetadataAddress(newUUID("1"))}, + {hrp: PrefixSession, ma: SessionMetadataAddress(newUUID("2"), newUUID("3"))}, + {hrp: PrefixRecord, ma: RecordMetadataAddress(newUUID("4"), newUUID("5").String())}, + {hrp: PrefixScopeSpecification, ma: ScopeSpecMetadataAddress(newUUID("6"))}, + {hrp: PrefixContractSpecification, ma: ContractSpecMetadataAddress(newUUID("7"))}, + {hrp: PrefixRecordSpecification, ma: RecordSpecMetadataAddress(newUUID("8"), newUUID("9").String())}, + } + for _, vc := range validCases { + for _, typeVC := range validCases { + tc := testCase{ + name: fmt.Sprintf("valid %s: want %s", vc.hrp, typeVC.hrp), + ma: vc.ma, + hrp: typeVC.hrp, + } + if vc.hrp != typeVC.hrp { + tc.expErr = fmt.Sprintf("invalid %s id \"%s\": wrong type", getNameForHRP(tc.hrp), tc.ma.String()) + } + tests = append(tests, tc) + } + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var err error + testFunc := func() { + err = VerifyMetadataAddressHasType(tc.ma, tc.hrp) + } + s.Require().NotPanics(testFunc, "VerifyMetadataAddressHasType(%q, %q)", tc.ma, tc.hrp) + assertions.AssertErrorValue(s.T(), err, tc.expErr, "error from VerifyMetadataAddressHasType(%q, %q)", tc.ma, tc.hrp) + }) + } +} + func (s *AddressTestSuite) TestMetadataAddressFromBech32() { notAScopeAddr := MetadataAddress{ScopeKeyPrefix[0], 1, 2, 3} notAScopeAddrStr, err := bech32.ConvertAndEncode(PrefixScope, notAScopeAddr) @@ -547,6 +720,103 @@ func (s *AddressTestSuite) TestMetadataAddressFromBech32() { } } +func (s *AddressTestSuite) TestMetadataAddressFromDenom() { + newUUID := func(name string, i int) uuid.UUID { + bz := []byte(fmt.Sprintf("%s[%d]________________", name, i))[:16] + rv, err := uuid.FromBytes(bz) + s.Require().NoError(err, "%s[%d]: uuid.FromBytes(%v)", name, i, bz) + return rv + } + scopeID := ScopeMetadataAddress(newUUID("scope", 0)) + sessionID := SessionMetadataAddress(newUUID("session", 1), newUUID("session", 2)) + recordID := RecordMetadataAddress(newUUID("record", 3), "money1") + scopeSpecID := ScopeSpecMetadataAddress(newUUID("scopespec", 4)) + contractSpecID := ContractSpecMetadataAddress(newUUID("contractspec", 5)) + recordSpecID := RecordSpecMetadataAddress(newUUID("recordspec", 6), "money2") + + tests := []struct { + name string + denom string + expAddr MetadataAddress + expErr string + }{ + { + name: "empty", + denom: "", + expErr: "denom \"\" is not a MetadataAddress denom", + }, + { + name: "non-medatadata denom", + denom: "nhash", + expErr: "denom \"nhash\" is not a MetadataAddress denom", + }, + { + name: "starts with nft without the slash", + denom: "nft" + scopeID.String(), + expErr: "denom \"nft" + scopeID.String() + "\" is not a MetadataAddress denom", + }, + { + name: "just the prefix", + denom: DenomPrefix, + expAddr: nil, + expErr: "invalid metadata address in denom \"nft/\": empty address string is not allowed", + }, + { + name: "invalid address", + denom: DenomPrefix + sdk.AccAddress("nope_nope_nope_nope_").String(), + expErr: "invalid metadata address in denom \"nft/" + sdk.AccAddress("nope_nope_nope_nope_").String() + "\": invalid metadata address type: 110", + }, + { + name: "just a scope id", + denom: scopeID.String(), + expErr: "denom \"" + scopeID.String() + "\" is not a MetadataAddress denom", + }, + { + name: "scope", + denom: scopeID.Denom(), + expAddr: scopeID, + }, + { + name: "session", + denom: sessionID.Denom(), + expAddr: sessionID, + }, + { + name: "record", + denom: recordID.Denom(), + expAddr: recordID, + }, + { + name: "scope spec", + denom: scopeSpecID.Denom(), + expAddr: scopeSpecID, + }, + { + name: "contract spec", + denom: contractSpecID.Denom(), + expAddr: contractSpecID, + }, + { + name: "record spec", + denom: recordSpecID.Denom(), + expAddr: recordSpecID, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var actAddr MetadataAddress + var actErr error + testFunc := func() { + actAddr, actErr = MetadataAddressFromDenom(tc.denom) + } + s.Require().NotPanics(testFunc, "MetadataAddressFromDenom(%q)", tc.denom) + assertions.AssertErrorValue(s.T(), actErr, tc.expErr, "error from MetadataAddressFromDenom(%q)", tc.denom) + s.Assert().Equal(tc.expAddr, actAddr, "address from MetadataAddressFromDenom(%q)", tc.denom) + }) + } +} + func (s *AddressTestSuite) TestMetadataAddressWithInvalidData() { t := s.T() @@ -1756,6 +2026,9 @@ func (s *AddressTestSuite) TestFormat() { emptyID := namedMetadataAddress{name: "empty", id: MetadataAddress{}} nilID := namedMetadataAddress{name: "nil", id: nil} invalidID := namedMetadataAddress{name: "invalid", id: MetadataAddress("do not create MetadataAddresses this way")} + expInvID := "MetadataAddress{0x64, 0x6f, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x20, " + + "0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x20, " + + "0x74, 0x68, 0x69, 0x73, 0x20, 0x77, 0x61, 0x79}" tests := []struct { id namedMetadataAddress @@ -1814,10 +2087,10 @@ func (s *AddressTestSuite) TestFormat() { {id: nilID, fmt: "%#v", exp: "MetadataAddress(nil)"}, {id: nilID, fmt: "%T", exp: "types.MetadataAddress"}, {id: nilID, fmt: "%x", exp: ""}, - {id: invalidID, fmt: "%s", exp: "%!s(PANIC=Format method: invalid metadata address type: 100)"}, - {id: invalidID, fmt: "%q", exp: "%!q(PANIC=Format method: invalid metadata address type: 100)"}, - {id: invalidID, fmt: "%v", exp: "%!v(PANIC=Format method: invalid metadata address type: 100)"}, - {id: invalidID, fmt: "%#v", exp: "MetadataAddress{0x64, 0x6f, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x20, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x77, 0x61, 0x79}"}, + {id: invalidID, fmt: "%s", exp: expInvID}, + {id: invalidID, fmt: "%q", exp: `"` + expInvID + `"`}, + {id: invalidID, fmt: "%v", exp: expInvID}, + {id: invalidID, fmt: "%#v", exp: expInvID}, {id: invalidID, fmt: "%T", exp: "types.MetadataAddress"}, {id: invalidID, fmt: "%x", exp: "646f206e6f7420637265617465204d65746164617461416464726573736573207468697320776179"}, } @@ -1845,6 +2118,89 @@ func (s *AddressTestSuite) TestFormat() { }) } +func (s *AddressTestSuite) TestValidateIsTypeAddressFuncs() { + newUUID := func(name string, i int) uuid.UUID { + bz := []byte(fmt.Sprintf("%s[%d]________________", name, i))[:16] + rv, err := uuid.FromBytes(bz) + s.Require().NoError(err, "%s[%d]: uuid.FromBytes(%q)", name, i, bz) + return rv + } + + funcsToTest := []struct { + name string + hrp string + validator func(ma MetadataAddress) error + }{ + { + name: "ValidateIsScopeAddress", + hrp: PrefixScope, + validator: MetadataAddress.ValidateIsScopeAddress, + }, + { + name: "ValidateIsScopeSpecificationAddress", + hrp: PrefixScopeSpecification, + validator: MetadataAddress.ValidateIsScopeSpecificationAddress, + }, + } + + addrsToTest := []struct { + name string + ma MetadataAddress + expInvalid string + }{ + { + name: "nil", + ma: nil, + expInvalid: "address is empty", + }, + { + name: "empty", + ma: MetadataAddress{}, + expInvalid: "address is empty", + }, + { + name: "unknown type", + ma: MetadataAddress{0xa0, 0x1, 0x2, 0xff}, + expInvalid: "invalid metadata address type: 160", + }, + { + name: "invalid scope", + ma: MetadataAddress{ScopeKeyPrefix[0], 0x1, 0x2, 0xff}, + expInvalid: "incorrect address length (expected: 17, actual: 4)", + }, + {name: PrefixScope, ma: ScopeMetadataAddress(newUUID("scope", 1))}, + {name: PrefixSession, ma: SessionMetadataAddress(newUUID("session", 2), newUUID("session", 3))}, + {name: PrefixRecord, ma: RecordMetadataAddress(newUUID("record", 4), "bananas")}, + {name: PrefixScopeSpecification, ma: ScopeSpecMetadataAddress(newUUID("scopespec", 5))}, + {name: PrefixContractSpecification, ma: ContractSpecMetadataAddress(newUUID("scopespec", 6))}, + {name: PrefixRecordSpecification, ma: RecordSpecMetadataAddress(newUUID("recordspec", 7), "alsobananas")}, + } + + for _, funcDef := range funcsToTest { + typeName := getNameForHRP(funcDef.hrp) + s.Run(funcDef.name, func() { + for _, addrDef := range addrsToTest { + s.Run(addrDef.name, func() { + var expErr string + switch { + case len(addrDef.expInvalid) > 0: + expErr = fmt.Sprintf("invalid %s metadata address %#v: %s", typeName, addrDef.ma, addrDef.expInvalid) + case funcDef.hrp != addrDef.name: + expErr = fmt.Sprintf("invalid %s id %q: wrong type", typeName, addrDef.ma) + } + + var err error + testFunc := func() { + err = funcDef.validator(addrDef.ma) + } + s.Require().NotPanics(testFunc, "%#v.%s()", addrDef.ma, funcDef.name) + assertions.AssertErrorValue(s.T(), err, expErr, "%#v.%s()", addrDef.ma, funcDef.name) + }) + } + }) + } +} + func (s *AddressTestSuite) TestGenerateExamples() { // This "test" doesn't actually test anything. It just generates some output that's helpful // for validating other MetadataAddress implementations. @@ -1885,3 +2241,1145 @@ func (s *AddressTestSuite) TestGenerateExamples() { } // TODO: GetDetails tests. + +func (s *AddressTestSuite) TestDenom() { + // As of writing this, the only metadata type that we should be making denoms for are scopes. + // However, I figured that restriction would be better left higher up which allows the Denom() method + // to be simpler (and wouldn't have to either return an error or panic). + // That's why I've included test cases for all the metadata types. + + // These were all generated in a terminal by running commands like this: + // $ provenanced metaaddress encode scope `uuidgen` + // I used `uuidgen` for the record names too because it was an easy way to get a random string. + tests := []string{ + "scope1qrrlhs80yxy5dwyp0tgmpvd49kmqn6cwx2", + "scope1qz4x0pmzt505ae4nmjfjq6ngq82q3g203c", + "session1q9xqvqjv73m5x245uwp3ut6jc6ykhs7xnvea7jxenx0pwtmu6cta6u48vr3", + "session1q93ef6tuvha5359suwj9svp0g2yqd8t72ky7vsfpnqx95aqc3rtnw57g9kn", + "record1qge4f7nh68tyvy473dlekp369lcfwax7ysuhsm3x7kql8mxxrcn85jjqy4m", + "record1q2r24x62aze5gxyws04qvxc9ey59kj7u6gsgcrljgcczkcuflc865ae0wzx", + "scopespec1qjjwrht7ne25cq9tua7tn0vtezcqrsneea", + "scopespec1qjzk96c9mjzy9z9zpjkuhvptujqsjhm5lz", + "contractspec1qw88nm7astdy9ay7vh8hc3jpur7qr27mh8", + "contractspec1qdez57m4vp2y4wd9qckuuhcp0lls0xx3nz", + "recspec1q48tu2exkajyafvn979hhe36ls5pv9jj9c08ldh7wtas4k0uj9xnzvgyg6e", + "recspec1q42c5g4a9k25zqyg6v95hp85sp2hy85aelha0l05yy635l2cp44mg3ca006", + } + + for i, tc := range tests { + s.Run(fmt.Sprintf("[%d]%s...%s", i, tc[:strings.Index(tc, "1")], tc[len(tc)-2:]), func() { + exp := DenomPrefix + tc + addr, err := MetadataAddressFromBech32(tc) + s.Require().NoError(err, "MetadataAddressFromBech32(%q)", tc) + + var act string + testFunc := func() { + act = addr.Denom() + } + s.Require().NotPanics(testFunc, "%#v.Denom()", addr) + s.Assert().Equal(exp, act, "%#v.Denom()", addr) + }) + } +} + +func (s *AddressTestSuite) TestCoin() { + // Just like the Denom tests, I'm testing all the types even though only scopes should really only ever be used. + + // These were copied from TestDenom. + tests := []string{ + "scope1qrrlhs80yxy5dwyp0tgmpvd49kmqn6cwx2", + "scope1qz4x0pmzt505ae4nmjfjq6ngq82q3g203c", + "session1q9xqvqjv73m5x245uwp3ut6jc6ykhs7xnvea7jxenx0pwtmu6cta6u48vr3", + "session1q93ef6tuvha5359suwj9svp0g2yqd8t72ky7vsfpnqx95aqc3rtnw57g9kn", + "record1qge4f7nh68tyvy473dlekp369lcfwax7ysuhsm3x7kql8mxxrcn85jjqy4m", + "record1q2r24x62aze5gxyws04qvxc9ey59kj7u6gsgcrljgcczkcuflc865ae0wzx", + "scopespec1qjjwrht7ne25cq9tua7tn0vtezcqrsneea", + "scopespec1qjzk96c9mjzy9z9zpjkuhvptujqsjhm5lz", + "contractspec1qw88nm7astdy9ay7vh8hc3jpur7qr27mh8", + "contractspec1qdez57m4vp2y4wd9qckuuhcp0lls0xx3nz", + "recspec1q48tu2exkajyafvn979hhe36ls5pv9jj9c08ldh7wtas4k0uj9xnzvgyg6e", + "recspec1q42c5g4a9k25zqyg6v95hp85sp2hy85aelha0l05yy635l2cp44mg3ca006", + } + + for i, tc := range tests { + s.Run(fmt.Sprintf("[%d]%s...%s", i, tc[:strings.Index(tc, "1")], tc[len(tc)-2:]), func() { + expCoin := sdk.Coin{Denom: DenomPrefix + tc, Amount: sdkmath.OneInt()} + expCoins := sdk.Coins{expCoin} + addr, err := MetadataAddressFromBech32(tc) + s.Require().NoError(err, "MetadataAddressFromBech32(%q)", tc) + + var actCoin sdk.Coin + testCoin := func() { + actCoin = addr.Coin() + } + if s.Assert().NotPanics(testCoin, "%#v.Coin()", addr) { + s.Assert().Equal(expCoin, actCoin, "%#v.Coin()", addr) + } + + var actCoins sdk.Coins + testCoins := func() { + actCoins = addr.Coins() + } + if s.Assert().NotPanics(testCoins, "%#v.Coins()", addr) { + s.Assert().Equal(expCoins, actCoins, "%#v.Coins()", addr) + } + }) + } +} + +func (s *AddressTestSuite) TestAccMDLink_String() { + newUUID := func(b byte) uuid.UUID { + bz := bytes.Repeat([]byte{b}, 16) + rv, err := uuid.FromBytes(bz) + s.Require().NoError(err, "uuid.FromBytes(%v)", bz) + return rv + } + makeExp := func(accStr, mdStr string) string { + return accStr + ":" + mdStr + } + accAddr := sdk.AccAddress("accAddr_____________") + scopeAddr := ScopeMetadataAddress(newUUID('0')) + sessionAddr := SessionMetadataAddress(newUUID('1'), newUUID('1')) + recordAddr := RecordMetadataAddress(newUUID('2'), strings.Repeat("2", 2)) + sSpecAddr := ScopeSpecMetadataAddress(newUUID('3')) + cSpecAddr := ContractSpecMetadataAddress(newUUID('4')) + rSpecAddr := RecordSpecMetadataAddress(newUUID('5'), strings.Repeat("5", 5)) + + tests := []struct { + name string + link *AccMDLink + exp string + }{ + { + name: "nil link", + link: nil, + exp: nilStr, + }, + { + name: "nil + nil", + link: NewAccMDLink(nil, nil), + exp: makeExp(nilStr, nilStr), + }, + { + name: "nil + empty", + link: NewAccMDLink(nil, MetadataAddress{}), + exp: makeExp(nilStr, emptyStr), + }, + { + name: "empty + nil", + link: NewAccMDLink(sdk.AccAddress{}, nil), + exp: makeExp(emptyStr, nilStr), + }, + { + name: "empty + empty", + link: NewAccMDLink(sdk.AccAddress{}, MetadataAddress{}), + exp: makeExp(emptyStr, emptyStr), + }, + { + name: "nil + scope", + link: NewAccMDLink(nil, scopeAddr), + exp: makeExp(nilStr, scopeAddr.String()), + }, + { + name: "empty + scope", + link: NewAccMDLink(sdk.AccAddress{}, scopeAddr), + exp: makeExp(emptyStr, scopeAddr.String()), + }, + { + name: "addr + nil", + link: NewAccMDLink(accAddr, nil), + exp: makeExp(accAddr.String(), nilStr), + }, + { + name: "addr + empty", + link: NewAccMDLink(accAddr, MetadataAddress{}), + exp: makeExp(accAddr.String(), emptyStr), + }, + { + name: "addr + scope", + link: NewAccMDLink(accAddr, scopeAddr), + exp: makeExp(accAddr.String(), scopeAddr.String()), + }, + { + name: "addr + session", + link: NewAccMDLink(accAddr, sessionAddr), + exp: makeExp(accAddr.String(), sessionAddr.String()), + }, + { + name: "addr + record", + link: NewAccMDLink(accAddr, recordAddr), + exp: makeExp(accAddr.String(), recordAddr.String()), + }, + { + name: "addr + scope spec", + link: NewAccMDLink(accAddr, sSpecAddr), + exp: makeExp(accAddr.String(), sSpecAddr.String()), + }, + { + name: "addr + contract spec", + link: NewAccMDLink(accAddr, cSpecAddr), + exp: makeExp(accAddr.String(), cSpecAddr.String()), + }, + { + name: "addr + record spec", + link: NewAccMDLink(accAddr, rSpecAddr), + exp: makeExp(accAddr.String(), rSpecAddr.String()), + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var act string + testFunc := func() { + act = tc.link.String() + } + s.Require().NotPanics(testFunc, "%v.String()", tc.link) + s.Assert().Equal(tc.exp, act, "$v.String()", tc.link) + }) + } +} + +func (s *AddressTestSuite) TestAccMDLinks_String() { + newUUID := func(b byte) uuid.UUID { + bz := bytes.Repeat([]byte{b}, 16) + rv, err := uuid.FromBytes(bz) + s.Require().NoError(err, "uuid.FromBytes(%v)", bz) + return rv + } + accAddr1 := sdk.AccAddress("accAddr1____________") + accAddr2 := sdk.AccAddress("accAddr2____________") + accAddr3 := sdk.AccAddress("accAddr3____________") + scopeAddr := ScopeMetadataAddress(newUUID('0')) + sessionAddr := SessionMetadataAddress(newUUID('1'), newUUID('1')) + recordAddr := RecordMetadataAddress(newUUID('2'), strings.Repeat("2", 2)) + sSpecAddr := ScopeSpecMetadataAddress(newUUID('3')) + cSpecAddr := ContractSpecMetadataAddress(newUUID('4')) + rSpecAddr := RecordSpecMetadataAddress(newUUID('5'), strings.Repeat("5", 5)) + makeExp := func(entries ...string) string { + return "[" + strings.Join(entries, ", ") + "]" + } + tests := []struct { + name string + links AccMDLinks + exp string + }{ + { + name: "nil", + links: nil, + exp: nilStr, + }, + { + name: "empty", + links: AccMDLinks{}, + exp: emptyStr, + }, + { + name: "one nil entry", + links: AccMDLinks{nil}, + exp: makeExp(nilStr), + }, + { + name: "one nil nil entry", + links: AccMDLinks{{}}, + exp: makeExp(NewAccMDLink(nil, nil).String()), + }, + { + name: "one empty empty entry", + links: AccMDLinks{NewAccMDLink(sdk.AccAddress{}, MetadataAddress{})}, + exp: makeExp(NewAccMDLink(sdk.AccAddress{}, MetadataAddress{}).String()), + }, + { + name: "one normal entry", + links: AccMDLinks{NewAccMDLink(accAddr1, scopeAddr)}, + exp: makeExp(NewAccMDLink(accAddr1, scopeAddr).String()), + }, + { + name: "many entries", + links: AccMDLinks{ + NewAccMDLink(accAddr1, scopeAddr), + NewAccMDLink(accAddr1, sessionAddr), + NewAccMDLink(accAddr2, recordAddr), + nil, + NewAccMDLink(accAddr2, sSpecAddr), + NewAccMDLink(accAddr3, cSpecAddr), + NewAccMDLink(accAddr3, rSpecAddr), + NewAccMDLink(accAddr1, nil), + NewAccMDLink(sdk.AccAddress{}, scopeAddr), + }, + exp: makeExp( + NewAccMDLink(accAddr1, scopeAddr).String(), + NewAccMDLink(accAddr1, sessionAddr).String(), + NewAccMDLink(accAddr2, recordAddr).String(), + nilStr, + NewAccMDLink(accAddr2, sSpecAddr).String(), + NewAccMDLink(accAddr3, cSpecAddr).String(), + NewAccMDLink(accAddr3, rSpecAddr).String(), + NewAccMDLink(accAddr1, nil).String(), + NewAccMDLink(sdk.AccAddress{}, scopeAddr).String(), + ), + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var act string + testFunc := func() { + act = tc.links.String() + } + s.Require().NotPanics(testFunc, "%v.String()", tc.links) + s.Assert().Equal(tc.exp, act, "$v.String()", tc.links) + }) + } +} + +func (s *AddressTestSuite) TestAccMDLinks_ValidateForScopes() { + newUUID := func(name string, i int) uuid.UUID { + bz := []byte(fmt.Sprintf("%s[%d]________________", name, i))[:16] + rv, err := uuid.FromBytes(bz) + s.Require().NoError(err, "%s[%d]: uuid.FromBytes(%v)", name, i, bz) + return rv + } + scopeIDs := make([]MetadataAddress, 6) + for i := range scopeIDs { + scopeIDs[i] = ScopeMetadataAddress(newUUID("scopeIDs", i)) + } + addrs := make([]sdk.AccAddress, len(scopeIDs)) + for i := range addrs { + addrs[i] = sdk.AccAddress(fmt.Sprintf("addr[%d]_____________", i)) + } + + tests := []struct { + name string + links AccMDLinks + exp string + }{ + { + name: "nil links", + links: nil, + exp: "", + }, + { + name: "empty links", + links: nil, + exp: "", + }, + { + name: "one link: nil", + links: AccMDLinks{nil}, + exp: "nil entry not allowed", + }, + { + name: "one link: empty", + links: AccMDLinks{{}}, + exp: "invalid scope metadata address MetadataAddress(nil): address is empty", + }, + { + name: "one link: nil md addr", + links: AccMDLinks{{MDAddr: nil, AccAddr: addrs[0]}}, + exp: "invalid scope metadata address MetadataAddress(nil): address is empty", + }, + { + name: "one link: empty md addr", + links: AccMDLinks{{MDAddr: MetadataAddress{}, AccAddr: addrs[0]}}, + exp: "invalid scope metadata address MetadataAddress{}: address is empty", + }, + { + name: "one link: scope", + links: AccMDLinks{{MDAddr: scopeIDs[0], AccAddr: addrs[0]}}, + exp: "", + }, + { + name: "one link: nil acc addr", + links: AccMDLinks{{MDAddr: scopeIDs[0], AccAddr: nil}}, + exp: fmt.Sprintf("no account address associated with metadata address %q", scopeIDs[0]), + }, + { + name: "one link: nil empty addr", + links: AccMDLinks{{MDAddr: scopeIDs[0], AccAddr: sdk.AccAddress{}}}, + exp: fmt.Sprintf("no account address associated with metadata address %q", scopeIDs[0]), + }, + { + name: "one link: session", + links: AccMDLinks{{MDAddr: SessionMetadataAddress(newUUID("session", 0), newUUID("session", 1)), AccAddr: addrs[0]}}, + exp: fmt.Sprintf("invalid scope id %q: wrong type", SessionMetadataAddress(newUUID("session", 0), newUUID("session", 1))), + }, + { + name: "one link: record", + links: AccMDLinks{{MDAddr: RecordMetadataAddress(newUUID("record", 0), "recordname"), AccAddr: addrs[0]}}, + exp: fmt.Sprintf("invalid scope id %q: wrong type", RecordMetadataAddress(newUUID("record", 0), "recordname")), + }, + { + name: "one link: scope spec", + links: AccMDLinks{{MDAddr: ScopeSpecMetadataAddress(newUUID("scopespec", 0)), AccAddr: addrs[0]}}, + exp: fmt.Sprintf("invalid scope id %q: wrong type", ScopeSpecMetadataAddress(newUUID("scopespec", 0))), + }, + { + name: "one link: contract spec", + links: AccMDLinks{{MDAddr: ContractSpecMetadataAddress(newUUID("contractspec", 0)), AccAddr: addrs[0]}}, + exp: fmt.Sprintf("invalid scope id %q: wrong type", ContractSpecMetadataAddress(newUUID("contractspec", 0))), + }, + { + name: "one link: record spec", + links: AccMDLinks{{MDAddr: RecordSpecMetadataAddress(newUUID("contractspec", 0), "recordname"), AccAddr: addrs[0]}}, + exp: fmt.Sprintf("invalid scope id %q: wrong type", RecordSpecMetadataAddress(newUUID("contractspec", 0), "recordname")), + }, + { + name: "one link: unknown mdaddr type", + links: AccMDLinks{{MDAddr: MetadataAddress{0xa0, 0x6e, 0x6f, 0x70, 0x65}, AccAddr: addrs[0]}}, + exp: "invalid scope metadata address MetadataAddress{0xa0, 0x6e, 0x6f, 0x70, 0x65}: invalid metadata address type: 160", + }, + { + name: "one link: scope type byte but invalid", + links: AccMDLinks{{MDAddr: MetadataAddress{ScopeKeyPrefix[0], 0x6e, 0x6f, 0x70, 0x65}, AccAddr: addrs[0]}}, + exp: "invalid scope metadata address MetadataAddress{0x0, 0x6e, 0x6f, 0x70, 0x65}: incorrect address length (expected: 17, actual: 5)", + }, + { + name: "two links: first nil", + links: AccMDLinks{nil, {MDAddr: scopeIDs[1], AccAddr: addrs[1]}}, + exp: "nil entry not allowed", + }, + { + name: "two links: first empty", + links: AccMDLinks{{}, {MDAddr: scopeIDs[1], AccAddr: addrs[1]}}, + exp: "invalid scope metadata address MetadataAddress(nil): address is empty", + }, + { + name: "two links: second nil", + links: AccMDLinks{{MDAddr: scopeIDs[0], AccAddr: addrs[0]}, nil}, + exp: "nil entry not allowed", + }, + { + name: "two links: second empty", + links: AccMDLinks{{MDAddr: scopeIDs[0], AccAddr: addrs[0]}, {}}, + exp: "invalid scope metadata address MetadataAddress(nil): address is empty", + }, + { + name: "two links: fully different", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: addrs[0]}, + {MDAddr: scopeIDs[1], AccAddr: addrs[1]}, + }, + exp: "", + }, + { + name: "two links: same scopes different acc addrs", + links: AccMDLinks{ + {MDAddr: scopeIDs[2], AccAddr: addrs[0]}, + {MDAddr: scopeIDs[2], AccAddr: addrs[1]}, + }, + exp: fmt.Sprintf("duplicate metadata address %q not allowed", scopeIDs[2]), + }, + { + name: "two links: same acc addrs different md addrs", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: addrs[0]}, + {MDAddr: scopeIDs[1], AccAddr: addrs[0]}, + }, + exp: "", + }, + { + name: "two links: invalid first md addr", + links: AccMDLinks{ + {MDAddr: ScopeSpecMetadataAddress(newUUID("scopespec", 1)), AccAddr: addrs[0]}, + {MDAddr: scopeIDs[1], AccAddr: addrs[1]}, + }, + exp: fmt.Sprintf("invalid scope id %q: wrong type", ScopeSpecMetadataAddress(newUUID("scopespec", 1))), + }, + { + name: "two links: invalid second md addr", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: addrs[0]}, + {MDAddr: MetadataAddress{0xa0, 0x6e, 0x6f, 0x70, 0x65}, AccAddr: addrs[1]}, + }, + exp: "invalid scope metadata address MetadataAddress{0xa0, 0x6e, 0x6f, 0x70, 0x65}: invalid metadata address type: 160", + }, + { + name: "two links: first missing acc addr", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: nil}, + {MDAddr: scopeIDs[1], AccAddr: addrs[1]}, + }, + exp: fmt.Sprintf("no account address associated with metadata address %q", scopeIDs[0]), + }, + { + name: "two links: second missing acc addr", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: addrs[0]}, + {MDAddr: scopeIDs[1], AccAddr: nil}, + }, + exp: fmt.Sprintf("no account address associated with metadata address %q", scopeIDs[1]), + }, + { + name: "six links: all valid and fully different", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: addrs[0]}, {MDAddr: scopeIDs[1], AccAddr: addrs[1]}, + {MDAddr: scopeIDs[2], AccAddr: addrs[2]}, {MDAddr: scopeIDs[3], AccAddr: addrs[3]}, + {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, {MDAddr: scopeIDs[5], AccAddr: addrs[5]}, + }, + exp: "", + }, + { + name: "six links: different scopes but same acc addrs", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: addrs[2]}, {MDAddr: scopeIDs[1], AccAddr: addrs[2]}, + {MDAddr: scopeIDs[2], AccAddr: addrs[2]}, {MDAddr: scopeIDs[3], AccAddr: addrs[2]}, + {MDAddr: scopeIDs[4], AccAddr: addrs[2]}, {MDAddr: scopeIDs[5], AccAddr: addrs[2]}, + }, + exp: "", + }, + { + name: "six links: same scopes but different acc addrs", + links: AccMDLinks{ + {MDAddr: scopeIDs[4], AccAddr: addrs[0]}, {MDAddr: scopeIDs[4], AccAddr: addrs[1]}, + {MDAddr: scopeIDs[4], AccAddr: addrs[2]}, {MDAddr: scopeIDs[4], AccAddr: addrs[3]}, + {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, {MDAddr: scopeIDs[4], AccAddr: addrs[5]}, + }, + exp: fmt.Sprintf("duplicate metadata address %q not allowed", scopeIDs[4]), + }, + { + name: "six links: all same", + links: AccMDLinks{ + {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, + {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, + {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, + }, + exp: fmt.Sprintf("duplicate metadata address %q not allowed", scopeIDs[4]), + }, + { + name: "six links: last is invalid md addr", + links: AccMDLinks{ + {MDAddr: scopeIDs[5], AccAddr: addrs[5]}, {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, + {MDAddr: scopeIDs[3], AccAddr: addrs[3]}, {MDAddr: scopeIDs[2], AccAddr: addrs[2]}, + {MDAddr: scopeIDs[1], AccAddr: addrs[1]}, {MDAddr: MetadataAddress{0xa0, 0x6e, 0x6f, 0x70, 0x65}, AccAddr: addrs[0]}, + }, + exp: "invalid scope metadata address MetadataAddress{0xa0, 0x6e, 0x6f, 0x70, 0x65}: invalid metadata address type: 160", + }, + { + name: "six links: last is missing acc addr", + links: AccMDLinks{ + {MDAddr: scopeIDs[1], AccAddr: addrs[1]}, {MDAddr: scopeIDs[0], AccAddr: addrs[0]}, + {MDAddr: scopeIDs[2], AccAddr: addrs[2]}, {MDAddr: scopeIDs[5], AccAddr: addrs[5]}, + {MDAddr: scopeIDs[3], AccAddr: addrs[3]}, {MDAddr: scopeIDs[4], AccAddr: nil}, + }, + exp: fmt.Sprintf("no account address associated with metadata address %q", scopeIDs[4]), + }, + { + name: "six links: last is dup scope", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: addrs[0]}, {MDAddr: scopeIDs[1], AccAddr: addrs[0]}, + {MDAddr: scopeIDs[2], AccAddr: addrs[2]}, {MDAddr: scopeIDs[3], AccAddr: addrs[3]}, + {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, {MDAddr: scopeIDs[3], AccAddr: addrs[5]}, + }, + exp: fmt.Sprintf("duplicate metadata address %q not allowed", scopeIDs[3]), + }, + { + name: "six links: last is nil", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: addrs[0]}, {MDAddr: scopeIDs[1], AccAddr: addrs[0]}, + {MDAddr: scopeIDs[2], AccAddr: addrs[2]}, {MDAddr: scopeIDs[3], AccAddr: addrs[3]}, + {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, nil, + }, + exp: "nil entry not allowed", + }, + { + name: "six links: last is empty", + links: AccMDLinks{ + {MDAddr: scopeIDs[0], AccAddr: addrs[0]}, {MDAddr: scopeIDs[1], AccAddr: addrs[0]}, + {MDAddr: scopeIDs[2], AccAddr: addrs[2]}, {MDAddr: scopeIDs[3], AccAddr: addrs[3]}, + {MDAddr: scopeIDs[4], AccAddr: addrs[4]}, {}, + }, + exp: "invalid scope metadata address MetadataAddress(nil): address is empty", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var err error + testFunc := func() { + err = tc.links.ValidateForScopes() + } + s.Require().NotPanics(testFunc, "ValidateForScopes") + assertions.AssertErrorValue(s.T(), err, tc.exp, "ValidateForScopes") + }) + } +} + +func (s *AddressTestSuite) TestAccMDLinks_GetAccAddrs() { + addr1 := sdk.AccAddress("1addr_______________") // cosmos1x9skgerjta047h6lta047h6lta047h6l4429yc + addr2 := sdk.AccAddress("2addr_______________") // cosmos1xfskgerjta047h6lta047h6lta047h6lh0rr9a + addr3 := sdk.AccAddress("3addr_______________") // cosmos1xdskgerjta047h6lta047h6lta047h6lw7ypa7 + + tests := []struct { + name string + links AccMDLinks + exp []sdk.AccAddress + }{ + { + name: "nil", + links: nil, + exp: nil, + }, + { + name: "empty", + links: AccMDLinks{}, + exp: nil, + }, + { + name: "one nil entry", + links: AccMDLinks{nil}, + exp: nil, + }, + { + name: "one entry: nil addr", + links: AccMDLinks{{AccAddr: nil}}, + exp: nil, + }, + { + name: "one entry: empty addr", + links: AccMDLinks{{AccAddr: sdk.AccAddress{}}}, + exp: nil, + }, + { + name: "one entry: ok addr", + links: AccMDLinks{{AccAddr: addr1}}, + exp: []sdk.AccAddress{addr1}, + }, + { + name: "two nil entries", + links: AccMDLinks{nil, nil}, + exp: nil, + }, + { + name: "two entries: addrs: nil nil", + links: AccMDLinks{{AccAddr: nil}, {AccAddr: nil}}, + exp: nil, + }, + { + name: "two entries: addrs: nil empty", + links: AccMDLinks{{AccAddr: nil}, {AccAddr: sdk.AccAddress{}}}, + exp: nil, + }, + { + name: "two entries: addrs: nil ok", + links: AccMDLinks{{AccAddr: nil}, {AccAddr: addr1}}, + exp: []sdk.AccAddress{addr1}, + }, + { + name: "two entries: nil entry, entry with ok address", + links: AccMDLinks{nil, {AccAddr: addr1}}, + exp: []sdk.AccAddress{addr1}, + }, + { + name: "two entries: addrs: empty nil", + links: AccMDLinks{{AccAddr: sdk.AccAddress{}}, {AccAddr: nil}}, + exp: nil, + }, + { + name: "two entries: addrs: empty empty", + links: AccMDLinks{{AccAddr: sdk.AccAddress{}}, {AccAddr: sdk.AccAddress{}}}, + exp: nil, + }, + { + name: "two entries: addrs: empty ok", + links: AccMDLinks{{AccAddr: sdk.AccAddress{}}, {AccAddr: addr1}}, + exp: []sdk.AccAddress{addr1}, + }, + { + name: "two entries: addrs: ok nil", + links: AccMDLinks{{AccAddr: addr1}, {AccAddr: nil}}, + exp: []sdk.AccAddress{addr1}, + }, + { + name: "two entries: ok addr then nil entry", + links: AccMDLinks{{AccAddr: addr1}, nil}, + exp: []sdk.AccAddress{addr1}, + }, + { + name: "two entries: addrs: ok empty", + links: AccMDLinks{{AccAddr: addr1}, {AccAddr: sdk.AccAddress{}}}, + exp: []sdk.AccAddress{addr1}, + }, + { + name: "two entries: addrs: same", + links: AccMDLinks{{AccAddr: addr1}, {AccAddr: addr1}}, + exp: []sdk.AccAddress{addr1}, + }, + { + name: "two entries: addrs: different", + links: AccMDLinks{{AccAddr: addr1}, {AccAddr: addr2}}, + exp: []sdk.AccAddress{addr1, addr2}, + }, + { + name: "two entries: addrs: different opposite order", + links: AccMDLinks{{AccAddr: addr2}, {AccAddr: addr1}}, + exp: []sdk.AccAddress{addr2, addr1}, + }, + { + name: "three different addrs with duplicates", + links: AccMDLinks{ + {AccAddr: addr2}, {AccAddr: addr2}, {AccAddr: addr1}, {AccAddr: addr2}, + {AccAddr: addr1}, {AccAddr: addr1}, {AccAddr: addr3}, {AccAddr: addr1}, + {AccAddr: addr2}, {AccAddr: addr1}, + }, + exp: []sdk.AccAddress{addr2, addr1, addr3}, + }, + { + name: "a bit of everything", + links: AccMDLinks{ + nil, nil, {AccAddr: nil}, {AccAddr: addr1}, + {AccAddr: addr1}, {AccAddr: sdk.AccAddress{}}, nil, {AccAddr: addr2}, + {AccAddr: addr1}, {AccAddr: nil}, {AccAddr: addr3}, {AccAddr: sdk.AccAddress{}}, + {AccAddr: addr3}, nil, {AccAddr: addr1}, nil, + }, + exp: []sdk.AccAddress{addr1, addr2, addr3}, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var act []sdk.AccAddress + testFunc := func() { + act = tc.links.GetAccAddrs() + } + s.Require().NotPanics(testFunc, "GetAccAddrs() on %s", tc.links.String()) + if !s.Assert().Equal(tc.exp, act, "result of GetAccAddrs()") { + expStrs := mapToStrings(tc.exp) + actStrs := mapToStrings(act) + s.Assert().Equal(expStrs, actStrs, "strings of the result of GetAccAddrs()") + } + }) + } +} + +func (s *AddressTestSuite) TestAccMDLinks_GetPrimaryUUIDs() { + newUUIDStr := func(i int) string { + s.Require().LessOrEqual(0, i, "arg provided to newUUID(%d)", i) + s.Require().GreaterOrEqual(15, i, "arg provided to newUUID(%d)", i) + h := '0' + byte(i) + if i >= 10 { + h = 'a' - 10 + byte(i) + } + return strings.ReplaceAll("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "x", string(h)) + } + newUUID := func(i int) uuid.UUID { + str := newUUIDStr(i) + rv, err := uuid.Parse(str) + s.Require().NoError(err, "uuid.Parse(%q)", str) + return rv + } + + tests := []struct { + name string + links AccMDLinks + exp []string + }{ + { + name: "nil links", + links: nil, + exp: nil, + }, + { + name: "empty links", + links: AccMDLinks{}, + exp: []string{}, + }, + { + name: "one link: nil", + links: AccMDLinks{nil}, + exp: []string{""}, + }, + { + name: "one link: nil addr", + links: AccMDLinks{{MDAddr: nil}}, + exp: []string{""}, + }, + { + name: "one link: empty addr", + links: AccMDLinks{{MDAddr: MetadataAddress{}}}, + exp: []string{""}, + }, + { + name: "one link: unknown type in addr", + links: AccMDLinks{{MDAddr: MetadataAddress{0xA0, 0x1, 0x2}}}, + exp: []string{""}, + }, + { + name: "one link: addr too short", + links: AccMDLinks{{MDAddr: ScopeMetadataAddress(newUUID(0))[:16]}}, + exp: []string{""}, + }, + { + name: "one link: scope id", + links: AccMDLinks{{MDAddr: ScopeMetadataAddress(newUUID(1))}}, + exp: []string{newUUIDStr(1)}, + }, + { + name: "one link: session id", + links: AccMDLinks{{MDAddr: SessionMetadataAddress(newUUID(2), newUUID(8))}}, + exp: []string{newUUIDStr(2)}, + }, + { + name: "one link: record id", + links: AccMDLinks{{MDAddr: RecordMetadataAddress(newUUID(3), newUUIDStr(4))}}, + exp: []string{newUUIDStr(3)}, + }, + { + name: "one link: scope spec id", + links: AccMDLinks{{MDAddr: ScopeSpecMetadataAddress(newUUID(5))}}, + exp: []string{newUUIDStr(5)}, + }, + { + name: "one link: contract spec id", + links: AccMDLinks{{MDAddr: ContractSpecMetadataAddress(newUUID(6))}}, + exp: []string{newUUIDStr(6)}, + }, + { + name: "one link: record spec id", + links: AccMDLinks{{MDAddr: RecordSpecMetadataAddress(newUUID(7), newUUIDStr(9))}}, + exp: []string{newUUIDStr(7)}, + }, + { + name: "six links: one of each type", + links: AccMDLinks{ + {MDAddr: ScopeMetadataAddress(newUUID(11))}, + {MDAddr: SessionMetadataAddress(newUUID(10), newUUID(3))}, + {MDAddr: RecordMetadataAddress(newUUID(15), newUUIDStr(2))}, + {MDAddr: ScopeSpecMetadataAddress(newUUID(13))}, + {MDAddr: ContractSpecMetadataAddress(newUUID(12))}, + {MDAddr: RecordSpecMetadataAddress(newUUID(14), newUUIDStr(1))}, + }, + exp: []string{ + newUUIDStr(11), newUUIDStr(10), newUUIDStr(15), + newUUIDStr(13), newUUIDStr(12), newUUIDStr(14), + }, + }, + { + name: "six links: all different scopes", + links: AccMDLinks{ + {MDAddr: ScopeMetadataAddress(newUUID(7))}, + {MDAddr: ScopeMetadataAddress(newUUID(1))}, + {MDAddr: ScopeMetadataAddress(newUUID(0))}, + {MDAddr: ScopeMetadataAddress(newUUID(8))}, + {MDAddr: ScopeMetadataAddress(newUUID(14))}, + {MDAddr: ScopeMetadataAddress(newUUID(2))}, + }, + exp: []string{ + newUUIDStr(7), newUUIDStr(1), newUUIDStr(0), + newUUIDStr(8), newUUIDStr(14), newUUIDStr(2), + }, + }, + { + name: "six links: mix of different, same and invalid scopes", + links: AccMDLinks{ + {MDAddr: ScopeMetadataAddress(newUUID(14))}, + {MDAddr: ScopeMetadataAddress(newUUID(12))}, + {MDAddr: ScopeMetadataAddress(newUUID(8))}, + {MDAddr: ScopeMetadataAddress(newUUID(10))[:16]}, + {MDAddr: ScopeMetadataAddress(newUUID(11))}, + {MDAddr: ScopeMetadataAddress(newUUID(12))}, + }, + exp: []string{ + newUUIDStr(14), newUUIDStr(12), newUUIDStr(8), + "", newUUIDStr(11), newUUIDStr(12), + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var act []string + testFunc := func() { + act = tc.links.GetPrimaryUUIDs() + } + s.Require().NotPanics(testFunc, "GetPrimaryUUIDs") + s.Assert().Equal(tc.exp, act, "result from GetPrimaryUUIDs") + }) + } +} + +func (s *AddressTestSuite) TestAccMDLinks_GetMDAddrsForAccAddr() { + newUUID := func(name string) uuid.UUID { + s.Require().LessOrEqual(len(name), 16, "newUUID(%q): name too long") + if len(name) < 16 { + name = name + strings.Repeat("_", 16-len(name)) + } + rv, err := uuid.FromBytes([]byte(name)) + s.Require().NoError(err, "uuid.FromBytes([]byte(%q))", name) + return rv + } + newAddr := func(name string) sdk.AccAddress { + switch { + case len(name) < 20: + name = name + strings.Repeat("_", 20-len(name)) + case len(name) > 20 && len(name) < 32: + name = name + strings.Repeat("_", 32-len(name)) + } + return sdk.AccAddress(name) + } + + accAddrs := []sdk.AccAddress{ + newAddr("0_addr"), // cosmos1xp0kzerywf047h6lta047h6lta047h6ln9q3ue + newAddr("1_addr"), // cosmos1x90kzerywf047h6lta047h6lta047h6l258ny6 + newAddr("2_addr"), // cosmos1xf0kzerywf047h6lta047h6lta047h6lgww49l + newAddr("3_addr"), // cosmos1xd0kzerywf047h6lta047h6lta047h6l3lfhau + newAddr("4_addr"), // cosmos1x30kzerywf047h6lta047h6lta047h6lvnue84 + } + testlog.WriteSlice(s.T(), "accAddrs", accAddrs) + + mdAddrs := []MetadataAddress{ + ScopeMetadataAddress(newUUID("0_scope")), // scope1qqc97umrdacx2h6lta047h6lta0s4e2vmr + ScopeMetadataAddress(newUUID("1_scope")), // scope1qqc47umrdacx2h6lta047h6lta0sfyvr90 + ScopeMetadataAddress(newUUID("2_scope")), // scope1qqe97umrdacx2h6lta047h6lta0sk6uj0g + ScopeMetadataAddress(newUUID("3_scope")), // scope1qqe47umrdacx2h6lta047h6lta0s286a3y + ScopeMetadataAddress(newUUID("4_scope")), // scope1qq697umrdacx2h6lta047h6lta0snl0e64 + SessionMetadataAddress(newUUID("5_session_1"), newUUID("5_session_2")), // session1qy647um9wdekjmmwtuc47h6lta0n2hmnv4ehx6t0de0nyh6lta047fgqzjx + RecordMetadataAddress(newUUID("6_record"), "6_record_name"), // record1qgm97un9vdhhyezlta047h6lta05sqnucqwnxlr6pxatcmq9sf0f5u999l2 + ScopeSpecMetadataAddress(newUUID("7_scope_spec")), // scopespec1qsm47umrdacx2hmnwpjkxh6lta0sz95anh + ContractSpecMetadataAddress(newUUID("8_contract_spec")), // contractspec1qvu97cm0de68yctrw30hxur9vd0smvmt09 + RecordSpecMetadataAddress(newUUID("9_record_spec"), "9_record_spec_name"), // recspec1q5u47un9vdhhyezlwdcx2c6lta0julrf7q442a5js2y8sm4gcnx8u98dcxq + } + testlog.WriteSlice(s.T(), "mdAddrs", mdAddrs) + + tests := []struct { + name string + links AccMDLinks + addr sdk.AccAddress + exp []MetadataAddress + }{ + { + name: "nil links", + links: nil, + addr: accAddrs[0], + exp: nil, + }, + { + name: "empty links", + links: make(AccMDLinks, 0), + addr: accAddrs[0], + exp: nil, + }, + { + name: "one link: nil", + links: AccMDLinks{nil}, + addr: nil, + exp: nil, + }, + { + name: "one link: nil AccAddr", + links: AccMDLinks{NewAccMDLink(nil, mdAddrs[0])}, + addr: accAddrs[0], + exp: nil, + }, + { + name: "one link: empty AccAddr", + links: AccMDLinks{NewAccMDLink(make(sdk.AccAddress, 0), mdAddrs[0])}, + addr: accAddrs[0], + exp: nil, + }, + { + name: "one link: other addr", + links: AccMDLinks{NewAccMDLink(accAddrs[0], mdAddrs[0])}, + addr: accAddrs[1], + exp: nil, + }, + { + name: "one link: same addr", + links: AccMDLinks{NewAccMDLink(accAddrs[0], mdAddrs[0])}, + addr: accAddrs[0], + exp: subSet(mdAddrs, 0), + }, + { + name: "two links with same AccAddr: other addr", + links: AccMDLinks{NewAccMDLink(accAddrs[2], mdAddrs[0]), NewAccMDLink(accAddrs[2], mdAddrs[1])}, + addr: accAddrs[3], + exp: nil, + }, + { + name: "two links with same AccAddr: that addr", + links: AccMDLinks{NewAccMDLink(accAddrs[2], mdAddrs[0]), NewAccMDLink(accAddrs[2], mdAddrs[1])}, + addr: accAddrs[2], + exp: subSet(mdAddrs, 0, 1), + }, + { + name: "two links with same AccAddr: that addr, opposite order", + links: AccMDLinks{NewAccMDLink(accAddrs[2], mdAddrs[1]), NewAccMDLink(accAddrs[2], mdAddrs[0])}, + addr: accAddrs[2], + exp: subSet(mdAddrs, 1, 0), + }, + { + name: "three links with diff AccAddr: get none", + links: AccMDLinks{ + NewAccMDLink(accAddrs[0], mdAddrs[0]), + NewAccMDLink(accAddrs[1], mdAddrs[1]), + NewAccMDLink(accAddrs[2], mdAddrs[2]), + }, + addr: accAddrs[3], + exp: nil, + }, + { + name: "three links with diff AccAddr: get first", + links: AccMDLinks{ + NewAccMDLink(accAddrs[0], mdAddrs[0]), + NewAccMDLink(accAddrs[1], mdAddrs[1]), + NewAccMDLink(accAddrs[2], mdAddrs[2]), + }, + addr: accAddrs[0], + exp: subSet(mdAddrs, 0), + }, + { + name: "three links with diff AccAddr: get second", + links: AccMDLinks{ + NewAccMDLink(accAddrs[0], mdAddrs[0]), + NewAccMDLink(accAddrs[1], mdAddrs[1]), + NewAccMDLink(accAddrs[2], mdAddrs[2]), + }, + addr: accAddrs[1], + exp: subSet(mdAddrs, 1), + }, + { + name: "three links with diff AccAddr: get third", + links: AccMDLinks{ + NewAccMDLink(accAddrs[0], mdAddrs[0]), + NewAccMDLink(accAddrs[1], mdAddrs[1]), + NewAccMDLink(accAddrs[2], mdAddrs[2]), + }, + addr: accAddrs[2], + exp: subSet(mdAddrs, 2), + }, + { + name: "three links two with same AccAddr: get first and second", + links: AccMDLinks{ + NewAccMDLink(accAddrs[3], mdAddrs[1]), + NewAccMDLink(accAddrs[3], mdAddrs[2]), + NewAccMDLink(accAddrs[0], mdAddrs[3]), + }, + addr: accAddrs[3], + exp: subSet(mdAddrs, 1, 2), + }, + { + name: "three links two with same AccAddr: get third", + links: AccMDLinks{ + NewAccMDLink(accAddrs[3], mdAddrs[1]), + NewAccMDLink(accAddrs[3], mdAddrs[2]), + NewAccMDLink(accAddrs[0], mdAddrs[3]), + }, + addr: accAddrs[0], + exp: subSet(mdAddrs, 3), + }, + { + name: "three links two with same AccAddr: get first and third", + links: AccMDLinks{ + NewAccMDLink(accAddrs[3], mdAddrs[1]), + NewAccMDLink(accAddrs[0], mdAddrs[2]), + NewAccMDLink(accAddrs[3], mdAddrs[3]), + }, + addr: accAddrs[3], + exp: subSet(mdAddrs, 1, 3), + }, + { + name: "three links two with same AccAddr: get second", + links: AccMDLinks{ + NewAccMDLink(accAddrs[3], mdAddrs[1]), + NewAccMDLink(accAddrs[0], mdAddrs[2]), + NewAccMDLink(accAddrs[3], mdAddrs[3]), + }, + addr: accAddrs[0], + exp: subSet(mdAddrs, 2), + }, + { + name: "three links two with same AccAddr: get second and third", + links: AccMDLinks{ + NewAccMDLink(accAddrs[0], mdAddrs[1]), + NewAccMDLink(accAddrs[3], mdAddrs[2]), + NewAccMDLink(accAddrs[3], mdAddrs[3]), + }, + addr: accAddrs[3], + exp: subSet(mdAddrs, 2, 3), + }, + { + name: "three links two with same AccAddr: get first", + links: AccMDLinks{ + NewAccMDLink(accAddrs[0], mdAddrs[1]), + NewAccMDLink(accAddrs[3], mdAddrs[2]), + NewAccMDLink(accAddrs[3], mdAddrs[3]), + }, + addr: accAddrs[0], + exp: subSet(mdAddrs, 1), + }, + { + name: "three links all with same AccAddr: get none", + links: AccMDLinks{ + NewAccMDLink(accAddrs[4], mdAddrs[1]), + NewAccMDLink(accAddrs[4], mdAddrs[2]), + NewAccMDLink(accAddrs[4], mdAddrs[3]), + }, + addr: accAddrs[3], + exp: nil, + }, + { + name: "three links all with same AccAddr: get all", + links: AccMDLinks{ + NewAccMDLink(accAddrs[4], mdAddrs[1]), + NewAccMDLink(accAddrs[4], mdAddrs[2]), + NewAccMDLink(accAddrs[4], mdAddrs[3]), + }, + addr: accAddrs[4], + exp: subSet(mdAddrs, 1, 2, 3), + }, + { + name: "five links: get two", + links: AccMDLinks{ + NewAccMDLink(accAddrs[0], mdAddrs[0]), + NewAccMDLink(accAddrs[1], mdAddrs[1]), + NewAccMDLink(accAddrs[2], mdAddrs[2]), + NewAccMDLink(accAddrs[1], mdAddrs[3]), + NewAccMDLink(accAddrs[4], mdAddrs[4]), + }, + addr: accAddrs[1], + exp: subSet(mdAddrs, 1, 3), + }, + { + name: "six links with diff MDAddr types: get three", + links: AccMDLinks{ + NewAccMDLink(accAddrs[0], mdAddrs[4]), + NewAccMDLink(accAddrs[1], mdAddrs[5]), + NewAccMDLink(accAddrs[0], mdAddrs[6]), + NewAccMDLink(accAddrs[2], mdAddrs[7]), + NewAccMDLink(accAddrs[2], mdAddrs[8]), + NewAccMDLink(accAddrs[0], mdAddrs[9]), + }, + addr: accAddrs[0], + exp: subSet(mdAddrs, 4, 6, 9), + }, + { + name: "six links with diff MDAddr types: get all", + links: AccMDLinks{ + NewAccMDLink(accAddrs[0], mdAddrs[4]), + NewAccMDLink(accAddrs[0], mdAddrs[5]), + NewAccMDLink(accAddrs[0], mdAddrs[6]), + NewAccMDLink(accAddrs[0], mdAddrs[7]), + NewAccMDLink(accAddrs[0], mdAddrs[8]), + NewAccMDLink(accAddrs[0], mdAddrs[9]), + }, + addr: accAddrs[0], + exp: subSet(mdAddrs, 4, 5, 6, 7, 8, 9), + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + var act []MetadataAddress + testFunc := func() { + act = tc.links.GetMDAddrsForAccAddr(tc.addr.String()) + } + s.Require().NotPanics(testFunc, "GetMDAddrsForAccAddr") + // Compare them as strings first since that failure message is probably easier to understand. + expStrs := mapToStrings(tc.exp) + actStrs := mapToStrings(act) + if s.Assert().Equal(expStrs, actStrs, "result of GetMDAddrsForAccAddr (as strings)") { + // The strings are equal, make sure that means they're actually equal. + s.Assert().Equal(tc.exp, act, "result of GetMDAddrsForAccAddr") + } + }) + } +} diff --git a/x/metadata/types/keys.go b/x/metadata/types/keys.go index 34fc0da2f9..50e4f37beb 100644 --- a/x/metadata/types/keys.go +++ b/x/metadata/types/keys.go @@ -14,9 +14,6 @@ const ( // RouterKey to be used for routing msgs RouterKey = ModuleName - - // DefaultParamspace is the name used for the parameter subspace for this module. - DefaultParamspace = ModuleName ) // KVStore Key Prefixes used for iterator/scans against the store and identification of key types @@ -73,8 +70,6 @@ var ( AddressScopeCacheKeyPrefix = []byte{0x17} // ScopeSpecScopeCacheKeyPrefix for scope to scope specification cache lookup ScopeSpecScopeCacheKeyPrefix = []byte{0x11} - // ValueOwnerScopeCacheKeyPrefix for scope to value owner address cache lookup - ValueOwnerScopeCacheKeyPrefix = []byte{0x18} // AddressScopeSpecCacheKeyPrefix for scope spec lookup by address AddressScopeSpecCacheKeyPrefix = []byte{0x19} @@ -113,16 +108,6 @@ func GetScopeSpecScopeCacheKey(scopeSpecID MetadataAddress, scopeID MetadataAddr return append(GetScopeSpecScopeCacheIteratorPrefix(scopeSpecID), scopeID.Bytes()...) } -// GetValueOwnerScopeCacheIteratorPrefix returns an iterator prefix for all scope cache entries assigned to a given address -func GetValueOwnerScopeCacheIteratorPrefix(addr sdk.AccAddress) []byte { - return append(ValueOwnerScopeCacheKeyPrefix, address.MustLengthPrefix(addr.Bytes())...) -} - -// GetValueOwnerScopeCacheKey returns the store key for an address cache entry -func GetValueOwnerScopeCacheKey(addr sdk.AccAddress, scopeID MetadataAddress) []byte { - return append(GetValueOwnerScopeCacheIteratorPrefix(addr), scopeID.Bytes()...) -} - // GetAddressScopeSpecCacheIteratorPrefix returns an iterator prefix for all scope spec cache entries assigned to a given address func GetAddressScopeSpecCacheIteratorPrefix(addr sdk.AccAddress) []byte { return append(AddressScopeSpecCacheKeyPrefix, address.MustLengthPrefix(addr.Bytes())...) @@ -158,7 +143,7 @@ func GetOSLocatorKey(addr sdk.AccAddress) []byte { return append(OSLocatorAddressKeyPrefix, address.MustLengthPrefix(addr.Bytes())...) } -// NetAssetValueKey returns key [prefix][scope address] for scope net asset values +// NetAssetValueKeyPrefix returns the [prefix][scope address] part of a scope net asset values key. func NetAssetValueKeyPrefix(scopeAddr MetadataAddress) []byte { return append(NetAssetValuePrefix, address.MustLengthPrefix(scopeAddr.Bytes())...) } diff --git a/x/metadata/types/scope.go b/x/metadata/types/scope.go index fe90bbbb91..f7611026e9 100644 --- a/x/metadata/types/scope.go +++ b/x/metadata/types/scope.go @@ -6,6 +6,8 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/provenance-io/provenance/internal/provutils" ) const ( @@ -44,22 +46,15 @@ func (s Scope) Equals(t Scope) bool { // ValidateBasic performs basic format checking of data within a scope func (s Scope) ValidateBasic() error { - prefix, err := VerifyMetadataAddressFormat(s.ScopeId) - if err != nil { + var err error + if err = s.ScopeId.ValidateIsScopeAddress(); err != nil { return err } - if prefix != PrefixScope { - return fmt.Errorf("invalid scope identifier (expected: %s, got %s)", PrefixScope, prefix) - } - if !s.SpecificationId.Empty() { - prefix, err = VerifyMetadataAddressFormat(s.SpecificationId) - if err != nil { - return err - } - if prefix != PrefixScopeSpecification { - return fmt.Errorf("invalid scope specification identifier (expected: %s, got %s)", PrefixScopeSpecification, prefix) - } + + if err = s.SpecificationId.ValidateIsScopeSpecificationAddress(); err != nil { + return err } + if err = s.ValidateOwnersBasic(); err != nil { return err } @@ -536,6 +531,12 @@ p1Loop: return true } +// FindMissingParties returns all parties from the required list that don't have a same party in the toCheck list. +// Uses the SamePartiers function to evaluate sameness. +func FindMissingParties(required, toCheck []Party) []Party { + return provutils.FindMissingFunc(required, toCheck, func(r, c Party) bool { return SamePartiers(&r, &c) }) +} + // GetPartyAddresses gets the addresses of all of the parties. Each address can only appear once in the return value. func GetPartyAddresses(parties []Party) []string { var rv []string diff --git a/x/metadata/types/scope.pb.go b/x/metadata/types/scope.pb.go index ac418b3b19..b19870a0e6 100644 --- a/x/metadata/types/scope.pb.go +++ b/x/metadata/types/scope.pb.go @@ -110,8 +110,20 @@ type Scope struct { Owners []Party `protobuf:"bytes,3,rep,name=owners,proto3" json:"owners"` // Addresses in this list are authorized to receive off-chain data associated with this scope. DataAccess []string `protobuf:"bytes,4,rep,name=data_access,json=dataAccess,proto3" json:"data_access,omitempty"` - // An address that controls the value associated with this scope. Standard blockchain accounts and marker accounts - // are supported for this value. This attribute may only be changed by the entity indicated once it is set. + // The address that controls the value associated with this scope. + // + // The value owner is actually tracked by the bank module using a coin with the denom "nft/". + // The value owner can be changed using WriteScope or anything that transfers funds, e.g. MsgSend. + // + // During WriteScope: + // - If this field is empty, it indicates that there should not be a change to the value owner. + // I.e. Once a scope has a value owner, it will always have one (until it's deleted). + // - If this field has a value, the existing value owner will be looked up, and + // - If there's already an existing value owner, they must be a signer, + // and the coin will be transferred to the new value owner. + // - If there isn't yet a value owner, the coin will be minted and sent to the new value owner. + // If the scope already exists, the owners must be signers (just like changing other fields). + // If it's a new scope, there's no special signer limitations related to the value owner. ValueOwnerAddress string `protobuf:"bytes,5,opt,name=value_owner_address,json=valueOwnerAddress,proto3" json:"value_owner_address,omitempty"` // Whether all parties in this scope and its sessions must be present in this scope's owners field. // This also enables use of optional=true scope owners and session parties. diff --git a/x/metadata/types/scope_test.go b/x/metadata/types/scope_test.go index edbfcaed1f..24832a30d4 100644 --- a/x/metadata/types/scope_test.go +++ b/x/metadata/types/scope_test.go @@ -2,6 +2,7 @@ package types import ( "encoding/hex" + "strings" "testing" "time" @@ -12,6 +13,8 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/provenance-io/provenance/testutil/assertions" ) type ScopeTestSuite struct { @@ -42,6 +45,12 @@ func OwnerPartyList(addresses ...string) []Party { } func (s *ScopeTestSuite) TestScopeValidateBasic() { + newUUID := func(i string) uuid.UUID { + uid := strings.ReplaceAll("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "x", i) + rv, err := uuid.Parse(uid) + s.Require().NoError(err, "uuid.Parse(%q)", uid) + return rv + } ns := func(scopeID, scopeSpecification MetadataAddress, owners []Party, dataAccess []string, valueOwner string) *Scope { return &Scope{ ScopeId: scopeID, @@ -53,9 +62,9 @@ func (s *ScopeTestSuite) TestScopeValidateBasic() { } } tests := []struct { - name string - scope *Scope - want string + name string + scope *Scope + expErr string }{ { "valid scope one owner", @@ -78,24 +87,29 @@ func (s *ScopeTestSuite) TestScopeValidateBasic() { "invalid scope owners: at least one party is required", }, { - "invalid scope id", - ns(ScopeSpecMetadataAddress(uuid.New()), ScopeSpecMetadataAddress(uuid.New()), []Party{}, []string{}, ""), - "invalid scope identifier (expected: scope, got scopespec)", + name: "invalid scope id", + scope: ns(MetadataAddress{0xa0, 0x1, 0x2}, ScopeSpecMetadataAddress(uuid.New()), []Party{}, []string{}, ""), + expErr: "invalid scope metadata address MetadataAddress{0xa0, 0x1, 0x2}: invalid metadata address type: 160", }, { - "invalid scope id - wrong address type", - ns(MetadataAddress{0x85}, ScopeSpecMetadataAddress(uuid.New()), []Party{}, []string{}, ""), - "invalid metadata address type: 133", + name: "invalid scope id - wrong address type", + scope: ns(ScopeSpecMetadataAddress(newUUID("1")), ScopeSpecMetadataAddress(uuid.New()), []Party{}, []string{}, ""), + expErr: "invalid scope id \"" + ScopeSpecMetadataAddress(newUUID("1")).String() + "\": wrong type", }, { - "invalid spec id", - ns(ScopeMetadataAddress(uuid.New()), ScopeMetadataAddress(uuid.New()), []Party{}, []string{}, ""), - "invalid scope specification identifier (expected: scopespec, got scope)", + name: "nil spec id", + scope: ns(ScopeMetadataAddress(uuid.New()), nil, []Party{}, []string{}, ""), + expErr: "invalid scope specification metadata address MetadataAddress(nil): address is empty", }, { - "invalid spec id - wrong address type", - ns(ScopeMetadataAddress(uuid.New()), MetadataAddress{0x85}, []Party{}, []string{}, ""), - "invalid metadata address type: 133", + name: "invalid spec id", + scope: ns(ScopeMetadataAddress(uuid.New()), MetadataAddress{0xa0, 0x1, 0x2}, []Party{}, []string{}, ""), + expErr: "invalid scope specification metadata address MetadataAddress{0xa0, 0x1, 0x2}: invalid metadata address type: 160", + }, + { + name: "invalid spec id - wrong address type", + scope: ns(ScopeMetadataAddress(uuid.New()), ScopeMetadataAddress(newUUID("2")), []Party{}, []string{}, ""), + expErr: "invalid scope specification id \"" + ScopeMetadataAddress(newUUID("2")).String() + "\": wrong type", }, { "invalid owner on scope", @@ -118,18 +132,18 @@ func (s *ScopeTestSuite) TestScopeValidateBasic() { ValueOwnerAddress: "", RequirePartyRollup: false, }, - want: "parties can only be optional when require_party_rollup = true", + expErr: "parties can only be optional when require_party_rollup = true", }, } for _, tc := range tests { s.Run(tc.name, func() { - err := tc.scope.ValidateBasic() - if len(tc.want) > 0 { - s.Assert().EqualError(err, tc.want, "ValidateBasic") - } else { - s.Assert().NoError(err, "ValidateBasic") + var err error + testFunc := func() { + err = tc.scope.ValidateBasic() } + s.Require().NotPanics(testFunc, "ValidateBasic") + assertions.AssertErrorValue(s.T(), err, tc.expErr, "error from ValidateBasic") }) } } @@ -1332,6 +1346,201 @@ func (s *ScopeTestSuite) TestEqualParties() { } } +func TestFindMissingParties(t *testing.T) { + // pz is just a shorter way to define a []Party + pz := func(parties ...Party) []Party { + return parties + } + + pOne3Req := Party{Address: "one", Role: 3, Optional: false} + pOne3Opt := Party{Address: "one", Role: 3, Optional: true} + pOne4Req := Party{Address: "one", Role: 4, Optional: false} + pOne4Opt := Party{Address: "one", Role: 4, Optional: true} + pTwo3Req := Party{Address: "two", Role: 3, Optional: false} + pTwo3Opt := Party{Address: "two", Role: 3, Optional: true} + pTwo4Req := Party{Address: "two", Role: 4, Optional: false} + pTwo4Opt := Party{Address: "two", Role: 4, Optional: true} + + // Note: PartyType_PARTY_TYPE_INVESTOR = 3, PartyType_PARTY_TYPE_CUSTODIAN = 4 + + tests := []struct { + name string + required []Party + toCheck []Party + expected []Party + }{ + { + name: "nil nil", + required: nil, + toCheck: nil, + expected: nil, + }, + { + name: "empty nil", + required: pz(), + toCheck: nil, + expected: nil, + }, + { + name: "nil empty", + required: nil, + toCheck: pz(), + expected: nil, + }, + { + name: "empty empty", + required: pz(), + toCheck: pz(), + expected: nil, + }, + + { + name: "nil VS one3", + required: nil, + toCheck: pz(pOne3Req), + expected: nil, + }, + { + name: "empty VS one3", + required: pz(), + toCheck: pz(pOne3Req), + expected: nil, + }, + + { + name: "one3req VS one3req", + required: pz(pOne3Req), + toCheck: pz(pOne3Req), + expected: nil, + }, + { + name: "one3req VS one3opt", + required: pz(pOne3Req), + toCheck: pz(pOne3Opt), + expected: nil, + }, + { + name: "one3opt VS one3req", + required: pz(pOne3Opt), + toCheck: pz(pOne3Req), + expected: nil, + }, + { + name: "one3opt VS one3opt", + required: pz(pOne3Opt), + toCheck: pz(pOne3Opt), + expected: nil, + }, + + { + name: "one3 one4 two3 two4 req VS one4 one3 two4 two3 req", + required: pz(pOne3Req, pOne4Req, pTwo3Req, pTwo4Req), + toCheck: pz(pOne4Req, pOne3Req, pTwo4Req, pTwo3Req), + expected: nil, + }, + { + name: "one3 one4 two3 two4 req VS one4 one3 two4 two3 opt", + required: pz(pOne3Req, pOne4Req, pTwo3Req, pTwo4Req), + toCheck: pz(pOne4Opt, pOne3Opt, pTwo4Opt, pTwo3Opt), + expected: nil, + }, + { + name: "one3 one4 two3 two4 opt vs one4 one3 two4 two3 req", + required: pz(pOne3Opt, pOne4Opt, pTwo3Opt, pTwo4Opt), + toCheck: pz(pOne4Req, pOne3Req, pTwo4Req, pTwo3Req), + expected: nil, + }, + { + name: "one3 one4 two3 two4 opt vs one4 one3 two4 two3 opt", + required: pz(pOne3Opt, pOne4Opt, pTwo3Opt, pTwo4Opt), + toCheck: pz(pOne4Opt, pOne3Opt, pTwo4Opt, pTwo3Opt), + expected: nil, + }, + + { + name: "one3 two4 VS nil", + required: pz(pOne3Opt, pTwo4Req), + toCheck: nil, + expected: pz(pOne3Opt, pTwo4Req), + }, + { + name: "one3 two4 VS empty", + required: pz(pOne3Opt, pTwo4Req), + toCheck: pz(), + expected: pz(pOne3Opt, pTwo4Req), + }, + { + name: "one3 two4 VS one3", + required: pz(pOne3Opt, pTwo4Req), + toCheck: pz(pOne3Req), + expected: pz(pTwo4Req), + }, + { + name: "one3 two4 VS one4", + required: pz(pOne3Opt, pTwo4Req), + toCheck: pz(pOne4Opt), + expected: pz(pOne3Opt, pTwo4Req), + }, + { + name: "one3 two4 VS two3", + required: pz(pOne3Opt, pTwo4Req), + toCheck: pz(pTwo3Opt), + expected: pz(pOne3Opt, pTwo4Req), + }, + { + name: "one3 two4 VS two4", + required: pz(pOne3Opt, pTwo4Req), + toCheck: pz(pTwo4Opt), + expected: pz(pOne3Opt), + }, + + { + name: "one3req two4opt VS two4req one3opt", + required: pz(pOne3Req, pTwo4Opt), + toCheck: pz(pTwo4Req, pOne3Opt), + expected: nil, + }, + { + name: "one3opt two4req VS two4opt one3req", + required: pz(pOne3Opt, pTwo4Req), + toCheck: pz(pTwo4Opt, pOne3Req), + expected: nil, + }, + + { + name: "one3opt VS all others req", + required: pz(pOne3Opt), + toCheck: pz(pOne3Req, pOne4Req, pTwo3Req, pTwo4Req), + expected: nil, + }, + { + name: "one3req VS all others opt", + required: pz(pOne3Req), + toCheck: pz(pOne3Opt, pOne4Opt, pTwo3Opt, pTwo4Opt), + expected: nil, + }, + { + name: "all req VS two3Opt", + required: pz(pOne4Req, pTwo3Req, pOne3Req, pTwo4Req), + toCheck: pz(pTwo3Opt), + expected: pz(pOne4Req, pOne3Req, pTwo4Req), + }, + { + name: "all opt VS two3Req", + required: pz(pOne4Opt, pOne3Opt, pTwo3Opt, pTwo4Opt), + toCheck: pz(pTwo3Req), + expected: pz(pOne4Opt, pOne3Opt, pTwo4Opt), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := FindMissingParties(tc.required, tc.toCheck) + assert.Equal(t, tc.expected, actual, "findMissingParties") + }) + } +} + type otherParty struct { address string role PartyType diff --git a/x/metadata/types/signer_utils.go b/x/metadata/types/signer_utils.go new file mode 100644 index 0000000000..151d28e81a --- /dev/null +++ b/x/metadata/types/signer_utils.go @@ -0,0 +1,429 @@ +package types + +import ( + "bytes" + "fmt" + "slices" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" +) + +// PartyDetails is a struct used to help process party and signer validation. +type PartyDetails struct { + // address is the bech32 account address string of this party. + address string + // role is the type of this party. + role PartyType + // optional indicates whether this party's signature is required (false => required, true => not required). + optional bool + + // acc is the converted address field. + acc sdk.AccAddress + + // signer is the bech32 string of the account signing for this party. + // It might be different from the address field if authz is involved. + signer string + // signerAcc is the account that's signing for this party. + // It might be different from the acc field if authz is involved. + signerAcc sdk.AccAddress + + // canBeUsedBySpec indicates whether this party can be used to fulfill a role required by the spec. + canBeUsedBySpec bool + // usedBySpec indicates whether this party has been used to fulfill a role required by the spec. + usedBySpec bool +} + +// WrapRequiredParty creates a PartyDetails from the provided Party. +func WrapRequiredParty(party Party) *PartyDetails { + return &PartyDetails{ + address: party.Address, + role: party.Role, + optional: party.Optional, + } +} + +// WrapAvailableParty creates a PartyDetails from the provided Party and marks it as optional and usable. +func WrapAvailableParty(party Party) *PartyDetails { + return &PartyDetails{ + address: party.Address, + role: party.Role, + optional: true, // An available party is optional unless something else says otherwise. + canBeUsedBySpec: true, + } +} + +// isSameAsF returns a function for use with slices.ContainsFunc to see if the given party is in a []*PartyDetails. +func isSameAsF(party Party) func(known *PartyDetails) bool { + return func(known *PartyDetails) bool { + return party.IsSameAs(known) + } +} + +// BuildPartyDetails creates the list of PartyDetails to be used in party/signer/role validation. +func BuildPartyDetails(reqParties, availableParties []Party) []*PartyDetails { + details := make([]*PartyDetails, 0, len(availableParties)) + + // Start with creating details for each available party. + for _, party := range availableParties { + if !slices.ContainsFunc(details, isSameAsF(party)) { + details = append(details, WrapAvailableParty(party)) + } + } + + // Now update the details to include optional=false required parties. + // If an equal party is already in the details, just update its optional flag + // to false, otherwise, add it to the list. + for _, reqParty := range reqParties { + if reqParty.Optional { + continue + } + if i := slices.IndexFunc(details, isSameAsF(reqParty)); i >= 0 { + details[i].MakeRequired() + continue + } + details = append(details, WrapRequiredParty(reqParty)) + } + + return details +} + +// Copy returns a copy of this PartyDetails. +func (p *PartyDetails) Copy() *PartyDetails { + if p == nil { + return nil + } + rv := &PartyDetails{ + address: p.address, + role: p.role, + optional: p.optional, + acc: nil, + signer: p.signer, + signerAcc: nil, + canBeUsedBySpec: p.canBeUsedBySpec, + usedBySpec: p.usedBySpec, + } + if p.acc != nil { + rv.acc = make(sdk.AccAddress, len(p.acc)) + copy(rv.acc, p.acc) + } + if p.signerAcc != nil { + rv.signerAcc = make(sdk.AccAddress, len(p.signerAcc)) + copy(rv.signerAcc, p.signerAcc) + } + return rv +} + +func (p *PartyDetails) SetAddress(address string) { + if p.address != address { + p.acc = nil + } + p.address = address +} + +func (p *PartyDetails) GetAddress() string { + if len(p.address) == 0 && len(p.acc) > 0 { + p.address = p.acc.String() + } + return p.address +} + +func (p *PartyDetails) SetAcc(addr sdk.AccAddress) { + if !bytes.Equal(p.acc, addr) { + p.address = "" + } + p.acc = addr +} + +func (p *PartyDetails) GetAcc() sdk.AccAddress { + if len(p.acc) == 0 && len(p.address) > 0 { + p.acc, _ = sdk.AccAddressFromBech32(p.address) + } + return p.acc +} + +func (p *PartyDetails) SetRole(role PartyType) { + p.role = role +} + +func (p *PartyDetails) GetRole() PartyType { + return p.role +} + +func (p *PartyDetails) SetOptional(optional bool) { + p.optional = optional +} + +func (p *PartyDetails) MakeRequired() { + p.optional = false +} + +func (p *PartyDetails) GetOptional() bool { + return p.optional +} + +func (p *PartyDetails) IsRequired() bool { + return !p.optional +} + +func (p *PartyDetails) SetSigner(signer string) { + if p.signer != signer { + p.signerAcc = nil + } + p.signer = signer +} + +func (p *PartyDetails) GetSigner() string { + if len(p.signer) == 0 && len(p.signerAcc) > 0 { + p.signer = p.signerAcc.String() + } + return p.signer +} + +func (p *PartyDetails) SetSignerAcc(signerAddr sdk.AccAddress) { + if !bytes.Equal(p.signerAcc, signerAddr) { + p.signer = "" + } + p.signerAcc = signerAddr +} + +func (p *PartyDetails) GetSignerAcc() sdk.AccAddress { + if len(p.signerAcc) == 0 && len(p.signer) > 0 { + p.signerAcc, _ = sdk.AccAddressFromBech32(p.signer) + } + return p.signerAcc +} + +func (p *PartyDetails) HasSigner() bool { + return len(p.signer) > 0 || len(p.signerAcc) > 0 +} + +func (p *PartyDetails) CanBeUsed() bool { + return p.canBeUsedBySpec +} + +func (p *PartyDetails) MarkAsUsed() { + p.usedBySpec = true +} + +func (p *PartyDetails) IsUsed() bool { + return p.usedBySpec +} + +// IsStillUsableAs returns true if this party can be used, hasn't yet been used and has the provided role. +func (p *PartyDetails) IsStillUsableAs(role PartyType) bool { + return p.CanBeUsed() && !p.IsUsed() && p.GetRole() == role +} + +// IsSameAs returns true if this is the same as the provided Party or PartyDetails. +// Only the address and role are considered for this test. +func (p *PartyDetails) IsSameAs(p2 Partier) bool { + return SamePartiers(p, p2) +} + +// GetUsedSigners gets a map of bech32 strings to true with a key for each used signer. +func GetUsedSigners(parties []*PartyDetails) UsedSignersMap { + rv := make(UsedSignersMap) + for _, party := range parties { + if party.HasSigner() { + rv.Use(party.GetSigner()) + } + } + return rv +} + +// TestablePartyDetails should only be used for testing. It's provides a way to customize the fields of a PartyDetails. +type TestablePartyDetails struct { + Address string + Role PartyType + Optional bool + Acc sdk.AccAddress + Signer string + SignerAcc sdk.AccAddress + CanBeUsedBySpec bool + UsedBySpec bool +} + +// NewTestablePartyDetails converts a PartyDetails into a TestablePartyDetails. +func NewTestablePartyDetails(pd *PartyDetails) TestablePartyDetails { + orig := pd.Copy() + return TestablePartyDetails{ + Address: orig.address, + Role: orig.role, + Optional: orig.optional, + Acc: orig.acc, + Signer: orig.signer, + SignerAcc: orig.signerAcc, + CanBeUsedBySpec: orig.canBeUsedBySpec, + UsedBySpec: orig.usedBySpec, + } +} + +// Real returns the PartyDetails version of this TestablePartyDetails. +func (p TestablePartyDetails) Real() *PartyDetails { + return &PartyDetails{ + address: p.Address, + role: p.Role, + optional: p.Optional, + acc: p.Acc, + signer: p.Signer, + signerAcc: p.SignerAcc, + canBeUsedBySpec: p.CanBeUsedBySpec, + usedBySpec: p.UsedBySpec, + } +} + +// UsedSignersMap is a type for recording that a signer has been used. +type UsedSignersMap map[string]bool + +// NewUsedSignersMap creates a new UsedSignersMap +func NewUsedSignersMap() UsedSignersMap { + return make(UsedSignersMap) +} + +// Use notes that the provided addresses have been used. +func (m UsedSignersMap) Use(addrs ...string) UsedSignersMap { + for _, addr := range addrs { + m[addr] = true + } + return m +} + +// IsUsed returns true if the provided address has been used. +func (m UsedSignersMap) IsUsed(addr string) bool { + return m[addr] +} + +// AlsoUse adds all the entries in the provided UsedSignersMap to this UsedSignersMap. +func (m UsedSignersMap) AlsoUse(m2 UsedSignersMap) UsedSignersMap { + for k := range m2 { + m[k] = true + } + return m +} + +// authzCacheAcceptableKey creates the key string used in the AuthzCache.acceptable map. +func authzCacheAcceptableKey(grantee, granter sdk.AccAddress, msgTypeURL string) string { + return string(grantee) + "-" + string(granter) + "-" + msgTypeURL +} + +// authzCacheIsWasmKey creates the key string used in the AuthzCache.known map. +func authzCacheIsWasmKey(addr sdk.AccAddress) string { + return string(addr) +} + +// AuthzCache is a struct that houses a map of authz authorizations that are known to have a passed Accept (and been handled). +type AuthzCache struct { + acceptable map[string]authz.Authorization + isWasm map[string]bool +} + +// NewAuthzCache creates a new AuthzCache. +func NewAuthzCache() *AuthzCache { + return &AuthzCache{ + acceptable: make(map[string]authz.Authorization), + isWasm: make(map[string]bool), + } +} + +// Clear deletes all entries from this AuthzCache. +func (c *AuthzCache) Clear() { + for k := range c.acceptable { + delete(c.acceptable, k) + } + for k := range c.isWasm { + delete(c.isWasm, k) + } +} + +// SetAcceptable sets an authorization in this cache as acceptable. +func (c *AuthzCache) SetAcceptable(grantee, granter sdk.AccAddress, msgTypeURL string, authorization authz.Authorization) { + c.acceptable[authzCacheAcceptableKey(grantee, granter, msgTypeURL)] = authorization +} + +// GetAcceptable gets a previously set acceptable authorization. +// Returns nil if no such authorization exists. +func (c *AuthzCache) GetAcceptable(grantee, granter sdk.AccAddress, msgTypeURL string) authz.Authorization { + return c.acceptable[authzCacheAcceptableKey(grantee, granter, msgTypeURL)] +} + +// SetIsWasm records whether an account is a wasm account. +func (c *AuthzCache) SetIsWasm(addr sdk.AccAddress, value bool) { + c.isWasm[authzCacheIsWasmKey(addr)] = value +} + +// HasIsWasm returns true if a cached IsWasm value has been recorded for the given address. +// Use GetIsWasm to get the previously recorded IsWasm value. +func (c *AuthzCache) HasIsWasm(addr sdk.AccAddress) bool { + _, rv := c.isWasm[authzCacheIsWasmKey(addr)] + return rv +} + +// GetIsWasm returns true if the address was previously recorded as being a wasm account. +// Returns false if either: +// - The address was previously recorded as NOT being a wasm account. +// - The WASM status of the account hasn't yet been recorded. +// +// Use HasIsWasm to differentiate the false conditions. +func (c *AuthzCache) GetIsWasm(addr sdk.AccAddress) bool { + return c.isWasm[authzCacheIsWasmKey(addr)] +} + +// GetAcceptableMap returns a copy of the map of acceptable authorizations in this AuthzCache. +// It only exists for unit testing purposes. +func (c *AuthzCache) GetAcceptableMap() map[string]authz.Authorization { + if c == nil || c.acceptable == nil { + return nil + } + rv := make(map[string]authz.Authorization, len(c.acceptable)) + for k, v := range c.acceptable { + rv[k] = v + } + return rv +} + +// GetIsWasmMap returns a copy of the map of previously made IsWasm checks. +// It only exists for unit testing purposes. +func (c *AuthzCache) GetIsWasmMap() map[string]bool { + if c == nil || c.isWasm == nil { + return nil + } + rv := make(map[string]bool, len(c.isWasm)) + for k, v := range c.isWasm { + rv[k] = v + } + return rv +} + +// authzCacheContextKey is the key used in an sdk.Context to set/get the AuthzCache. +const authzCacheContextKey = "authzCacheContextKey" + +// AddAuthzCacheToContext either returns a new sdk.Context with the addition of an AuthzCache, +// or clears out the AuthzCache if it already exists in the context. +// It panics if the AuthzCache key exists in the context but isn't an AuthzCache. +func AddAuthzCacheToContext(ctx sdk.Context) sdk.Context { + // If it's already got one, leave it there but clear it out. + // Otherwise, we'll add a new one. + if cacheV := ctx.Value(authzCacheContextKey); cacheV != nil { + if cache, ok := cacheV.(*AuthzCache); ok { + cache.Clear() + return ctx + } + // If the key was there, but not an AuthzCache, things are very wrong. Panic. + panic(fmt.Errorf("context value %q is a %T, expected %T", authzCacheContextKey, cacheV, NewAuthzCache())) + } + return ctx.WithValue(authzCacheContextKey, NewAuthzCache()) +} + +// GetAuthzCache gets the AuthzCache from the context or panics. +func GetAuthzCache(ctx sdk.Context) *AuthzCache { + cacheV := ctx.Value(authzCacheContextKey) + if cacheV == nil { + panic(fmt.Errorf("context does not contain a %q value", authzCacheContextKey)) + } + cache, ok := cacheV.(*AuthzCache) + if !ok { + panic(fmt.Errorf("context value %q is a %T, expected %T", authzCacheContextKey, cacheV, NewAuthzCache())) + } + return cache +} diff --git a/x/metadata/types/signer_utils_test.go b/x/metadata/types/signer_utils_test.go new file mode 100644 index 0000000000..aebd5e5a31 --- /dev/null +++ b/x/metadata/types/signer_utils_test.go @@ -0,0 +1,2470 @@ +package types + +import ( + "context" + "fmt" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + + "github.com/provenance-io/provenance/testutil/assertions" +) + +// emptySdkContext creates a new sdk.Context that only has an empty background context. +func emptySdkContext() sdk.Context { + return sdk.Context{}.WithContext(context.Background()) +} + +func TestWrapRequiredParty(t *testing.T) { + addr := sdk.AccAddress("just_a_test_address_").String() + tests := []struct { + name string + party Party + exp *PartyDetails + }{ + { + name: "control", + party: Party{ + Address: addr, + Role: PartyType_PARTY_TYPE_OWNER, + Optional: true, + }, + exp: &PartyDetails{ + address: addr, + role: PartyType_PARTY_TYPE_OWNER, + optional: true, + }, + }, + { + name: "zero", + party: Party{}, + exp: &PartyDetails{}, + }, + { + name: "address only", + party: Party{Address: addr}, + exp: &PartyDetails{address: addr}, + }, + { + name: "role only", + party: Party{Role: PartyType_PARTY_TYPE_INVESTOR}, + exp: &PartyDetails{role: PartyType_PARTY_TYPE_INVESTOR}, + }, + { + name: "optional only", + party: Party{Optional: true}, + exp: &PartyDetails{optional: true}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := WrapRequiredParty(tc.party) + assert.Equal(t, tc.exp, actual, "WrapRequiredParty") + }) + } +} + +func TestWrapAvailableParty(t *testing.T) { + addr := sdk.AccAddress("just_a_test_address_").String() + tests := []struct { + name string + party Party + exp *PartyDetails + }{ + { + name: "control", + party: Party{ + Address: addr, + Role: PartyType_PARTY_TYPE_OWNER, + Optional: true, + }, + exp: &PartyDetails{ + address: addr, + role: PartyType_PARTY_TYPE_OWNER, + optional: true, + canBeUsedBySpec: true, + }, + }, + { + name: "zero", + party: Party{}, + exp: &PartyDetails{ + optional: true, + canBeUsedBySpec: true, + }, + }, + { + name: "address only", + party: Party{Address: addr}, + exp: &PartyDetails{ + address: addr, + optional: true, + canBeUsedBySpec: true, + }, + }, + { + name: "role only", + party: Party{Role: PartyType_PARTY_TYPE_INVESTOR}, + exp: &PartyDetails{ + role: PartyType_PARTY_TYPE_INVESTOR, + optional: true, + canBeUsedBySpec: true, + }, + }, + { + name: "optional only", + party: Party{Optional: true}, + exp: &PartyDetails{ + optional: true, + canBeUsedBySpec: true, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := WrapAvailableParty(tc.party) + assert.Equal(t, tc.exp, actual, "WrapAvailableParty") + }) + } +} + +func TestBuildPartyDetails(t *testing.T) { + addr1 := sdk.AccAddress("this_is_address_1___").String() + addr2 := sdk.AccAddress("this_is_address_2___").String() + addr3 := sdk.AccAddress("this_is_address_3___").String() + + // pz is a short way to create a slice of parties. + pz := func(parties ...Party) []Party { + rv := make([]Party, 0, len(parties)) + rv = append(rv, parties...) + return rv + } + // dz is a short way to create a slice of PartyDetails + pdz := func(parties ...*PartyDetails) []*PartyDetails { + rv := make([]*PartyDetails, 0, len(parties)) + rv = append(rv, parties...) + return rv + } + tests := []struct { + name string + reqParties []Party + availableParties []Party + exp []*PartyDetails + }{ + { + name: "nil nil", + reqParties: nil, + availableParties: nil, + exp: pdz(), + }, + { + name: "nil empty", + reqParties: nil, + availableParties: pz(), + exp: pdz(), + }, + { + name: "nil one", + reqParties: nil, + availableParties: pz(Party{Address: addr1, Role: 3, Optional: false}), + exp: pdz(&PartyDetails{ + address: addr1, + role: 3, + optional: true, + canBeUsedBySpec: true, + }), + }, + { + name: "empty nil", + reqParties: pz(), + availableParties: nil, + exp: pdz(), + }, + { + name: "empty empty", + reqParties: pz(), + availableParties: pz(), + exp: pdz(), + }, + { + name: "empty one", + reqParties: pz(), + availableParties: pz(Party{Address: addr1, Role: 3, Optional: false}), + exp: pdz(&PartyDetails{ + address: addr1, + role: 3, + optional: true, + canBeUsedBySpec: true, + }), + }, + { + name: "one nil", + reqParties: pz(Party{Address: addr1, Role: 5, Optional: false}), + availableParties: nil, + exp: pdz(&PartyDetails{ + address: addr1, + role: 5, + optional: false, + }), + }, + { + name: "one empty", + reqParties: pz(Party{Address: addr1, Role: 5, Optional: false}), + availableParties: pz(), + exp: pdz(&PartyDetails{ + address: addr1, + role: 5, + optional: false, + }), + }, + { + name: "one one different role and address", + reqParties: pz(Party{Address: addr1, Role: 5, Optional: false}), + availableParties: pz(Party{Address: addr2, Role: 4, Optional: false}), + exp: pdz( + &PartyDetails{ + address: addr2, + role: 4, + optional: true, + canBeUsedBySpec: true, + }, + &PartyDetails{ + address: addr1, + role: 5, + optional: false, + }, + ), + }, + { + name: "one one different role same address", + reqParties: pz(Party{Address: addr1, Role: 5, Optional: false}), + availableParties: pz(Party{Address: addr1, Role: 4, Optional: false}), + exp: pdz( + &PartyDetails{ + address: addr1, + role: 4, + optional: true, + canBeUsedBySpec: true, + }, + &PartyDetails{ + address: addr1, + role: 5, + optional: false, + }, + ), + }, + { + name: "one one different address same role", + reqParties: pz(Party{Address: addr1, Role: 5, Optional: false}), + availableParties: pz(Party{Address: addr2, Role: 5, Optional: false}), + exp: pdz( + &PartyDetails{ + address: addr2, + role: 5, + optional: true, + canBeUsedBySpec: true, + }, + &PartyDetails{ + address: addr1, + role: 5, + optional: false, + }, + ), + }, + { + name: "one one same address and role", + reqParties: pz(Party{Address: addr1, Role: 5, Optional: false}), + availableParties: pz(Party{Address: addr1, Role: 5, Optional: true}), + exp: pdz(&PartyDetails{ + address: addr1, + role: 5, + optional: false, + canBeUsedBySpec: true, + }), + }, + { + name: "two two with one same", + reqParties: pz( + Party{Address: addr3, Role: 1, Optional: false}, + Party{Address: addr2, Role: 7, Optional: false}, + ), + availableParties: pz( + Party{Address: addr1, Role: 5, Optional: true}, + Party{Address: addr2, Role: 7, Optional: true}, + ), + exp: pdz( + &PartyDetails{ + address: addr1, + role: 5, + optional: true, + canBeUsedBySpec: true, + }, + &PartyDetails{ + address: addr2, + role: 7, + optional: false, + canBeUsedBySpec: true, + }, + &PartyDetails{ + address: addr3, + role: 1, + optional: false, + }, + ), + }, + { + name: "duplicate req parties", + reqParties: pz( + Party{Address: addr1, Role: 2, Optional: false}, + Party{Address: addr1, Role: 2, Optional: false}, + ), + availableParties: nil, + exp: pdz(&PartyDetails{ + address: addr1, + role: 2, + optional: false, + }), + }, + { + name: "duplicate available parties", + reqParties: nil, + availableParties: pz( + Party{Address: addr1, Role: 3, Optional: false}, + Party{Address: addr1, Role: 3, Optional: false}, + ), + exp: pdz(&PartyDetails{ + address: addr1, + role: 3, + optional: true, + canBeUsedBySpec: true, + }), + }, + { + name: "two req parties one optional", + reqParties: pz( + Party{Address: addr1, Role: 2, Optional: false}, + Party{Address: addr2, Role: 3, Optional: true}, + ), + availableParties: nil, + exp: pdz(&PartyDetails{ + address: addr1, + role: 2, + optional: false, + }), + }, + { + name: "two req parties one optional also in available", + reqParties: pz( + Party{Address: addr1, Role: 2, Optional: false}, + Party{Address: addr2, Role: 3, Optional: true}, + ), + availableParties: pz(Party{Address: addr2, Role: 3, Optional: false}), + exp: pdz( + &PartyDetails{ + address: addr2, + role: 3, + optional: true, + canBeUsedBySpec: true, + }, + &PartyDetails{ + address: addr1, + role: 2, + optional: false, + }, + ), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := BuildPartyDetails(tc.reqParties, tc.availableParties) + assert.Equal(t, tc.exp, actual, "BuildPartyDetails") + }) + } +} + +func TestPartyDetails_Copy(t *testing.T) { + tests := []struct { + name string + pd *PartyDetails + }{ + { + name: "nil", + pd: nil, + }, + { + name: "empty", + pd: &PartyDetails{}, + }, + { + name: "just address", + pd: &PartyDetails{address: "address_value"}, + }, + { + name: "just role", + pd: &PartyDetails{role: PartyType_PARTY_TYPE_OMNIBUS}, + }, + { + name: "just optional", + pd: &PartyDetails{optional: true}, + }, + { + name: "just acc", + pd: &PartyDetails{acc: sdk.AccAddress("acc_value")}, + }, + { + name: "just signer", + pd: &PartyDetails{signer: "signer_value"}, + }, + { + name: "just signerAcc", + pd: &PartyDetails{signerAcc: sdk.AccAddress("signerAcc_value")}, + }, + { + name: "just canBeUsedBySpec", + pd: &PartyDetails{canBeUsedBySpec: true}, + }, + { + name: "just usedBySpec", + pd: &PartyDetails{usedBySpec: true}, + }, + { + name: "required party", + pd: WrapRequiredParty(Party{ + Address: "required_address", + Role: PartyType_PARTY_TYPE_CUSTODIAN, + Optional: false, + }), + }, + { + name: "available party", + pd: WrapAvailableParty(Party{ + Address: "available_address", + Role: PartyType_PARTY_TYPE_ORIGINATOR, + Optional: true, + }), + }, + { + name: "everything populated", + pd: &PartyDetails{ + address: "another_address", + role: PartyType_PARTY_TYPE_AFFILIATE, + optional: true, + acc: sdk.AccAddress("the_acc_field"), + signer: "another_signer", + signerAcc: sdk.AccAddress("the_signerAcc_field"), + canBeUsedBySpec: true, + usedBySpec: true, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual *PartyDetails + testFunc := func() { + actual = tc.pd.Copy() + } + require.NotPanics(t, testFunc, "Copy()") + require.Equal(t, tc.pd, actual, "result of Copy()") + if tc.pd != nil && actual != nil { + assert.NotSame(t, tc.pd, actual, "result of Copy()") + assert.NotSame(t, tc.pd.acc, actual.acc, "acc field") + if len(actual.acc) > 0 { + // Change the first byte in the copy to make sure it doesn't also change in the original. + actual.acc[0] = actual.acc[0] + 1 + assert.NotEqual(t, tc.pd.acc, actual.acc, "the acc field after changing it in the copy") + // And put it back so we don't mess up anything else. + actual.acc[0] = actual.acc[0] - 1 + } + assert.NotSame(t, tc.pd.signerAcc, actual.signerAcc, "signerAcc field") + if len(actual.signerAcc) > 0 { + // Change the first byte in the copy to make sure it doesn't also change in the original. + actual.signerAcc[0] = actual.signerAcc[0] + 1 + assert.NotEqual(t, tc.pd.signerAcc, actual.signerAcc, "the signerAcc field after changing it in the copy") + actual.signerAcc[0] = actual.signerAcc[0] - 1 + } + } + }) + } +} + +func TestPartyDetails_SetAddress(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(address string, acc sdk.AccAddress) *PartyDetails { + return &PartyDetails{ + address: address, + acc: acc, + } + } + + addrAcc := sdk.AccAddress("settable_tst_address") + addr := addrAcc.String() + + tests := []struct { + name string + party *PartyDetails + addr string + expParty *PartyDetails + }{ + { + name: "unset to set", + party: pd("", nil), + addr: addr, + expParty: pd(addr, nil), + }, + { + name: "set to unset", + party: pd(addr, addrAcc), + addr: "", + expParty: pd("", nil), + }, + { + name: "changing to non-acc", + party: pd(addr, addrAcc), + addr: "new-address", + expParty: pd("new-address", nil), + }, + { + name: "changing from non-acc", + party: pd("not-an-acc", addrAcc), + addr: addr, + expParty: pd(addr, nil), + }, + { + name: "not changing", + party: pd(addr, sdk.AccAddress("something else")), + addr: addr, + expParty: pd(addr, sdk.AccAddress("something else")), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.party.SetAddress(tc.addr) + assert.Equal(t, tc.expParty, tc.party, "party after SetAddress") + }) + } +} + +func TestPartyDetails_GetAddress(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(address string, acc sdk.AccAddress) *PartyDetails { + return &PartyDetails{ + address: address, + acc: acc, + } + } + + addrAcc := sdk.AccAddress("settable_tst_address") + addr := addrAcc.String() + + tests := []struct { + name string + party *PartyDetails + exp string + expParty *PartyDetails + }{ + { + name: "no address no acc", + party: pd("", nil), + exp: "", + expParty: pd("", nil), + }, + { + name: "address without acc", + party: pd(addr, nil), + exp: addr, + expParty: pd(addr, nil), + }, + { + name: "invalid address without acc", + party: pd("invalid", nil), + exp: "invalid", + expParty: pd("invalid", nil), + }, + { + name: "invalid address with acc", + party: pd("invalid", addrAcc), + exp: "invalid", + expParty: pd("invalid", addrAcc), + }, + { + name: "acc without address", + party: pd("", addrAcc), + exp: addr, + expParty: pd(addr, addrAcc), + }, + { + name: "address with different acc", + party: pd(addr, sdk.AccAddress("different_acc_______")), + exp: addr, + expParty: pd(addr, sdk.AccAddress("different_acc_______")), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.GetAddress() + assert.Equal(t, tc.exp, actual, "GetAddress") + assert.Equal(t, tc.expParty, tc.party, "party after GetAddress") + }) + } +} + +func TestPartyDetails_SetAcc(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(address string, acc sdk.AccAddress) *PartyDetails { + return &PartyDetails{ + address: address, + acc: acc, + } + } + + addrAcc := sdk.AccAddress("settable_tst_address") + addr := addrAcc.String() + + tests := []struct { + name string + party *PartyDetails + addr sdk.AccAddress + expParty *PartyDetails + }{ + { + name: "unset to set", + party: pd("", nil), + addr: addrAcc, + expParty: pd("", addrAcc), + }, + { + name: "set to unset", + party: pd(addr, addrAcc), + addr: nil, + expParty: pd("", nil), + }, + { + name: "changing no address", + party: pd("", addrAcc), + addr: sdk.AccAddress("new_address_________"), + expParty: pd("", sdk.AccAddress("new_address_________")), + }, + { + name: "changing have address", + party: pd(addr, addrAcc), + addr: sdk.AccAddress("new_address_________"), + expParty: pd("", sdk.AccAddress("new_address_________")), + }, + { + name: "not changing", + party: pd("something else", addrAcc), + addr: addrAcc, + expParty: pd("something else", addrAcc), + }, + { + name: "nil to empty", + party: pd("foo", nil), + addr: sdk.AccAddress{}, + expParty: pd("foo", sdk.AccAddress{}), + }, + { + name: "empty to nil", + party: pd("foo", sdk.AccAddress{}), + addr: nil, + expParty: pd("foo", nil), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.party.SetAcc(tc.addr) + assert.Equal(t, tc.expParty, tc.party, "party after SetAcc") + }) + } +} + +func TestPartyDetails_GetAcc(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(address string, acc sdk.AccAddress) *PartyDetails { + return &PartyDetails{ + address: address, + acc: acc, + } + } + + addrAcc := sdk.AccAddress("settable_tst_address") + addr := addrAcc.String() + + tests := []struct { + name string + party *PartyDetails + exp sdk.AccAddress + expParty *PartyDetails + }{ + { + name: "no address nil acc", + party: pd("", nil), + exp: nil, + expParty: pd("", nil), + }, + { + name: "no address empty acc", + party: pd("", sdk.AccAddress{}), + exp: sdk.AccAddress{}, + expParty: pd("", sdk.AccAddress{}), + }, + { + name: "address without acc", + party: pd(addr, nil), + exp: addrAcc, + expParty: pd(addr, addrAcc), + }, + { + name: "invalid address without acc", + party: pd("invalid", nil), + exp: nil, + expParty: pd("invalid", nil), + }, + { + name: "invalid address with acc", + party: pd("invalid", addrAcc), + exp: addrAcc, + expParty: pd("invalid", addrAcc), + }, + { + name: "acc without address", + party: pd("", addrAcc), + exp: addrAcc, + expParty: pd("", addrAcc), + }, + { + name: "address with different acc", + party: pd(addr, sdk.AccAddress("different_acc_______")), + exp: sdk.AccAddress("different_acc_______"), + expParty: pd(addr, sdk.AccAddress("different_acc_______")), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.GetAcc() + assert.Equal(t, tc.exp, actual, "GetAcc") + assert.Equal(t, tc.expParty, tc.party, "party after GetAcc") + }) + } +} + +func TestPartyDetails_SetRole(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(role PartyType) *PartyDetails { + return &PartyDetails{role: role} + } + + tests := []struct { + name string + party *PartyDetails + role PartyType + expParty *PartyDetails + }{ + { + name: "unset to set", + party: pd(0), + role: 1, + expParty: pd(1), + }, + { + name: "set to unset", + party: pd(2), + role: 0, + expParty: pd(0), + }, + { + name: "changing", + party: pd(3), + role: 8, + expParty: pd(8), + }, + { + name: "not changing", + party: pd(4), + role: 4, + expParty: pd(4), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.party.SetRole(tc.role) + assert.Equal(t, tc.expParty, tc.party, "party after SetRole") + }) + } +} + +func TestPartyDetails_GetRole(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(role PartyType) *PartyDetails { + return &PartyDetails{role: role} + } + + type testCase struct { + name string + party *PartyDetails + exp PartyType + } + + var tests []testCase + for r := range PartyType_name { + role := PartyType(r) + tests = append(tests, testCase{ + name: role.SimpleString(), + party: pd(role), + exp: role, + }) + } + sort.Slice(tests, func(i, j int) bool { + return tests[i].party.GetRole() < tests[j].party.GetRole() + }) + tests = append(tests, testCase{ + name: "invalid role", + party: pd(-8), + exp: -8, + }) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.GetRole() + assert.Equal(t, tc.exp.SimpleString(), actual.SimpleString(), "GetRole") + }) + } +} + +func TestPartyDetails_SetOptional(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(optional bool) *PartyDetails { + return &PartyDetails{optional: optional} + } + + tests := []struct { + name string + party *PartyDetails + optional bool + expParty *PartyDetails + }{ + { + name: "true to true", + party: pd(true), + optional: true, + expParty: pd(true), + }, + { + name: "true to false", + party: pd(true), + optional: false, + expParty: pd(false), + }, + { + name: "false to true", + party: pd(false), + optional: true, + expParty: pd(true), + }, + { + name: "false to false", + party: pd(false), + optional: false, + expParty: pd(false), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.party.SetOptional(tc.optional) + assert.Equal(t, tc.expParty, tc.party, "party after SetOptional") + }) + } +} + +func TestPartyDetails_MakeRequired(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(optional bool) *PartyDetails { + return &PartyDetails{optional: optional} + } + + tests := []struct { + name string + party *PartyDetails + expParty *PartyDetails + }{ + { + name: "from optional", + party: pd(true), + expParty: pd(false), + }, + { + name: "from required", + party: pd(false), + expParty: pd(false), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.party.MakeRequired() + assert.Equal(t, tc.expParty, tc.party, "party after MakeRequired") + }) + } +} + +func TestPartyDetails_GetOptional(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(optional bool) *PartyDetails { + return &PartyDetails{optional: optional} + } + + tests := []struct { + name string + party *PartyDetails + exp bool + expParty *PartyDetails + }{ + { + name: "optional", + party: pd(true), + exp: true, + expParty: pd(true), + }, + { + name: "required", + party: pd(false), + exp: false, + expParty: pd(false), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.GetOptional() + assert.Equal(t, tc.exp, actual, "GetOptional") + assert.Equal(t, tc.expParty, tc.party, "party after GetOptional") + }) + } +} + +func TestPartyDetails_IsRequired(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(optional bool) *PartyDetails { + return &PartyDetails{optional: optional} + } + + tests := []struct { + name string + party *PartyDetails + exp bool + expParty *PartyDetails + }{ + { + name: "optional", + party: pd(true), + exp: false, + expParty: pd(true), + }, + { + name: "required", + party: pd(false), + exp: true, + expParty: pd(false), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.IsRequired() + assert.Equal(t, tc.exp, actual, "IsRequired") + assert.Equal(t, tc.expParty, tc.party, "party after IsRequired") + }) + } +} + +func TestPartyDetails_SetSigner(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(signer string, signerAcc sdk.AccAddress) *PartyDetails { + return &PartyDetails{ + signer: signer, + signerAcc: signerAcc, + } + } + + addrAcc := sdk.AccAddress("settable_tst_address") + addr := addrAcc.String() + + tests := []struct { + name string + party *PartyDetails + signer string + expParty *PartyDetails + }{ + { + name: "unset to set", + party: pd("", nil), + signer: addr, + expParty: pd(addr, nil), + }, + { + name: "set to unset", + party: pd(addr, addrAcc), + signer: "", + expParty: pd("", nil), + }, + { + name: "changing to non-acc", + party: pd(addr, addrAcc), + signer: "new-address", + expParty: pd("new-address", nil), + }, + { + name: "changing from non-acc", + party: pd("not-an-acc", addrAcc), + signer: addr, + expParty: pd(addr, nil), + }, + { + name: "not changing", + party: pd(addr, sdk.AccAddress("something else")), + signer: addr, + expParty: pd(addr, sdk.AccAddress("something else")), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.party.SetSigner(tc.signer) + assert.Equal(t, tc.expParty, tc.party, "party after SetSigner") + }) + } +} + +func TestPartyDetails_GetSigner(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(signer string, signerAcc sdk.AccAddress) *PartyDetails { + return &PartyDetails{ + signer: signer, + signerAcc: signerAcc, + } + } + + addrAcc := sdk.AccAddress("settable_tst_address") + addr := addrAcc.String() + + tests := []struct { + name string + party *PartyDetails + exp string + expParty *PartyDetails + }{ + { + name: "no address no acc", + party: pd("", nil), + exp: "", + expParty: pd("", nil), + }, + { + name: "address without acc", + party: pd(addr, nil), + exp: addr, + expParty: pd(addr, nil), + }, + { + name: "invalid address without acc", + party: pd("invalid", nil), + exp: "invalid", + expParty: pd("invalid", nil), + }, + { + name: "invalid address with acc", + party: pd("invalid", addrAcc), + exp: "invalid", + expParty: pd("invalid", addrAcc), + }, + { + name: "acc without address", + party: pd("", addrAcc), + exp: addr, + expParty: pd(addr, addrAcc), + }, + { + name: "address with different acc", + party: pd(addr, sdk.AccAddress("different_acc_______")), + exp: addr, + expParty: pd(addr, sdk.AccAddress("different_acc_______")), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.GetSigner() + assert.Equal(t, tc.exp, actual, "GetSigner") + assert.Equal(t, tc.expParty, tc.party, "party after GetSigner") + }) + } +} + +func TestPartyDetails_SetSignerAcc(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(signer string, signerAcc sdk.AccAddress) *PartyDetails { + return &PartyDetails{ + signer: signer, + signerAcc: signerAcc, + } + } + + addrAcc := sdk.AccAddress("settable_tst_address") + addr := addrAcc.String() + + tests := []struct { + name string + party *PartyDetails + signer sdk.AccAddress + expParty *PartyDetails + }{ + { + name: "unset to set", + party: pd("", nil), + signer: addrAcc, + expParty: pd("", addrAcc), + }, + { + name: "set to unset", + party: pd(addr, addrAcc), + signer: nil, + expParty: pd("", nil), + }, + { + name: "changing no address", + party: pd("", addrAcc), + signer: sdk.AccAddress("new_address_________"), + expParty: pd("", sdk.AccAddress("new_address_________")), + }, + { + name: "changing have address", + party: pd(addr, addrAcc), + signer: sdk.AccAddress("new_address_________"), + expParty: pd("", sdk.AccAddress("new_address_________")), + }, + { + name: "not changing", + party: pd("something else", addrAcc), + signer: addrAcc, + expParty: pd("something else", addrAcc), + }, + { + name: "nil to empty", + party: pd("foo", nil), + signer: sdk.AccAddress{}, + expParty: pd("foo", sdk.AccAddress{}), + }, + { + name: "empty to nil", + party: pd("foo", sdk.AccAddress{}), + signer: nil, + expParty: pd("foo", nil), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.party.SetSignerAcc(tc.signer) + assert.Equal(t, tc.expParty, tc.party, "party after SetSignerAcc") + }) + } +} + +func TestPartyDetails_GetSignerAcc(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(signer string, signerAcc sdk.AccAddress) *PartyDetails { + return &PartyDetails{ + signer: signer, + signerAcc: signerAcc, + } + } + + addrAcc := sdk.AccAddress("settable_tst_address") + addr := addrAcc.String() + + tests := []struct { + name string + party *PartyDetails + exp sdk.AccAddress + expParty *PartyDetails + }{ + { + name: "no address nil acc", + party: pd("", nil), + exp: nil, + expParty: pd("", nil), + }, + { + name: "no address empty acc", + party: pd("", sdk.AccAddress{}), + exp: sdk.AccAddress{}, + expParty: pd("", sdk.AccAddress{}), + }, + { + name: "address without acc", + party: pd(addr, nil), + exp: addrAcc, + expParty: pd(addr, addrAcc), + }, + { + name: "invalid address without acc", + party: pd("invalid", nil), + exp: nil, + expParty: pd("invalid", nil), + }, + { + name: "invalid address with acc", + party: pd("invalid", addrAcc), + exp: addrAcc, + expParty: pd("invalid", addrAcc), + }, + { + name: "acc without address", + party: pd("", addrAcc), + exp: addrAcc, + expParty: pd("", addrAcc), + }, + { + name: "address with different acc", + party: pd(addr, sdk.AccAddress("different_acc_______")), + exp: sdk.AccAddress("different_acc_______"), + expParty: pd(addr, sdk.AccAddress("different_acc_______")), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.GetSignerAcc() + assert.Equal(t, tc.exp, actual, "GetSignerAcc") + assert.Equal(t, tc.expParty, tc.party, "party after GetSignerAcc") + }) + } +} + +func TestPartyDetails_HasSigner(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(signer string, signerAcc sdk.AccAddress) *PartyDetails { + return &PartyDetails{ + signer: signer, + signerAcc: signerAcc, + } + } + + tests := []struct { + name string + party *PartyDetails + exp bool + expParty *PartyDetails + }{ + { + name: "no string or acc", + party: pd("", nil), + exp: false, + expParty: pd("", nil), + }, + { + name: "string no acc", + party: pd("a", nil), + exp: true, + expParty: pd("a", nil), + }, + { + name: "acc no string", + party: pd("", sdk.AccAddress("b")), + exp: true, + expParty: pd("", sdk.AccAddress("b")), + }, + { + name: "string and acc", + party: pd("a", sdk.AccAddress("b")), + exp: true, + expParty: pd("a", sdk.AccAddress("b")), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.HasSigner() + assert.Equal(t, tc.exp, actual, "HasSigner") + assert.Equal(t, tc.expParty, tc.party, "party after HasSigner") + }) + } +} + +func TestPartyDetails_CanBeUsed(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(canBeUsedBySpec bool) *PartyDetails { + return &PartyDetails{canBeUsedBySpec: canBeUsedBySpec} + } + + tests := []struct { + name string + party *PartyDetails + exp bool + expParty *PartyDetails + }{ + { + name: "can be used", + party: pd(true), + exp: true, + expParty: pd(true), + }, + { + name: "cannot be used", + party: pd(false), + exp: false, + expParty: pd(false), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.CanBeUsed() + assert.Equal(t, tc.exp, actual, "CanBeUsed") + assert.Equal(t, tc.expParty, tc.party, "party after CanBeUsed") + }) + } +} + +func TestPartyDetails_MarkAsUsed(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(usedBySpec bool) *PartyDetails { + return &PartyDetails{usedBySpec: usedBySpec} + } + + tests := []struct { + name string + party *PartyDetails + expParty *PartyDetails + }{ + { + name: "from not used", + party: pd(false), + expParty: pd(true), + }, + { + name: "from used", + party: pd(true), + expParty: pd(true), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.party.MarkAsUsed() + assert.Equal(t, tc.expParty, tc.party, "party after MarkAsUsed") + }) + } +} + +func TestPartyDetails_IsUsed(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(usedBySpec bool) *PartyDetails { + return &PartyDetails{usedBySpec: usedBySpec} + } + + tests := []struct { + name string + party *PartyDetails + exp bool + expParty *PartyDetails + }{ + { + name: "used", + party: pd(true), + exp: true, + expParty: pd(true), + }, + { + name: "not used", + party: pd(false), + exp: false, + expParty: pd(false), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.IsUsed() + assert.Equal(t, tc.exp, actual, "IsUsed") + assert.Equal(t, tc.expParty, tc.party, "party after IsUsed") + }) + } +} + +func TestPartyDetails_IsStillUsableAs(t *testing.T) { + // pd is a short way to create a PartyDetails with only what we care about in this test. + pd := func(role PartyType, canBeUsedBySpec, usedBySpec bool) *PartyDetails { + return &PartyDetails{ + role: role, + canBeUsedBySpec: canBeUsedBySpec, + usedBySpec: usedBySpec, + } + } + + tests := []struct { + name string + party *PartyDetails + role PartyType + exp bool + expParty *PartyDetails + }{ + { + name: "same role can be used is not used", + party: pd(1, true, false), + role: 1, + exp: true, + expParty: pd(1, true, false), + }, + { + name: "same role can be used is used", + party: pd(1, true, true), + role: 1, + exp: false, + expParty: pd(1, true, true), + }, + { + name: "same role cannot be used is not used", + party: pd(1, false, false), + role: 1, + exp: false, + expParty: pd(1, false, false), + }, + { + name: "same role cannot be used is used", + party: pd(1, false, true), + role: 1, + exp: false, + expParty: pd(1, false, true), + }, + { + name: "diff role can be used is not used", + party: pd(1, true, false), + role: 2, + exp: false, + expParty: pd(1, true, false), + }, + { + name: "diff role can be used is used", + party: pd(1, true, true), + role: 2, + exp: false, + expParty: pd(1, true, true), + }, + { + name: "diff role cannot be used is not used", + party: pd(1, false, false), + role: 2, + exp: false, + expParty: pd(1, false, false), + }, + { + name: "diff role cannot be used is used", + party: pd(1, false, true), + role: 2, + exp: false, + expParty: pd(1, false, true), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.IsStillUsableAs(tc.role) + assert.Equal(t, tc.exp, actual, "IsStillUsableAs(%s)", tc.role.SimpleString()) + assert.Equal(t, tc.expParty, tc.party, "party after IsStillUsableAs") + }) + } +} + +func TestPartyDetails_IsSameAs(t *testing.T) { + tests := []struct { + name string + party *PartyDetails + p2 Partier + exp bool + expParty *PartyDetails + }{ + { + name: "party details same addr and role all others different", + party: &PartyDetails{ + address: "same", + role: 1, + optional: false, + acc: sdk.AccAddress("one_________________"), + signer: "signer1", + signerAcc: sdk.AccAddress("signer1_____________"), + canBeUsedBySpec: false, + usedBySpec: false, + }, + p2: &PartyDetails{ + address: "same", + role: 1, + optional: true, + acc: sdk.AccAddress("two_________________"), + signer: "signer2", + signerAcc: sdk.AccAddress("signer2_____________"), + canBeUsedBySpec: true, + usedBySpec: true, + }, + exp: true, + expParty: &PartyDetails{ + address: "same", + role: 1, + optional: false, + acc: sdk.AccAddress("one_________________"), + signer: "signer1", + signerAcc: sdk.AccAddress("signer1_____________"), + canBeUsedBySpec: false, + usedBySpec: false, + }, + }, + { + name: "party same addr and role different optional", + party: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + p2: &Party{ + Address: "same", + Role: 1, + Optional: true, + }, + exp: true, + expParty: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + }, + { + name: "same but only have acc", + party: &PartyDetails{ + acc: sdk.AccAddress("same_acc_address____"), + role: 1, + optional: false, + }, + p2: &Party{ + Address: sdk.AccAddress("same_acc_address____").String(), + Role: 1, + Optional: true, + }, + exp: true, + expParty: &PartyDetails{ + address: sdk.AccAddress("same_acc_address____").String(), + acc: sdk.AccAddress("same_acc_address____"), + role: 1, + optional: false, + }, + }, + { + name: "same but both only have acc", + party: &PartyDetails{ + acc: sdk.AccAddress("same_acc_address____"), + role: 1, + optional: false, + }, + p2: &PartyDetails{ + acc: sdk.AccAddress("same_acc_address____"), + role: 1, + optional: false, + }, + exp: true, + expParty: &PartyDetails{ + address: sdk.AccAddress("same_acc_address____").String(), + acc: sdk.AccAddress("same_acc_address____"), + role: 1, + optional: false, + }, + }, + { + name: "party details different address", + party: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + p2: &PartyDetails{ + address: "not same", + role: 1, + optional: true, + }, + exp: false, + expParty: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + }, + { + name: "party details different role", + party: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + p2: &PartyDetails{ + address: "same", + role: 2, + optional: true, + }, + exp: false, + expParty: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + }, + { + name: "party different address", + party: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + p2: &Party{ + Address: "not same", + Role: 1, + Optional: true, + }, + exp: false, + expParty: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + }, + { + name: "party different role", + party: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + p2: &Party{ + Address: "same", + Role: 2, + Optional: true, + }, + exp: false, + expParty: &PartyDetails{ + address: "same", + role: 1, + optional: false, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.party.IsSameAs(tc.p2) + assert.Equal(t, tc.exp, actual, "IsSameAs") + assert.Equal(t, tc.expParty, tc.party, "party after IsSameAs") + }) + } +} + +func TestGetUsedSigners(t *testing.T) { + addr := func(str string) sdk.AccAddress { + if len(str) == 0 { + return nil + } + return sdk.AccAddress(str) + } + addrStr := func(str string) string { + if len(str) == 0 { + return "" + } + return addr(str).String() + } + pd := func(address, signer, signerAcc string) *PartyDetails { + return &PartyDetails{ + address: addrStr(address), + signer: addrStr(signer), + signerAcc: addr(signerAcc), + } + } + pdz := func(parties ...*PartyDetails) []*PartyDetails { + rv := make([]*PartyDetails, 0, len(parties)) + rv = append(rv, parties...) + return parties + } + + tests := []struct { + name string + parties []*PartyDetails + exp UsedSignersMap + }{ + { + name: "nil parties", + parties: nil, + exp: map[string]bool{}, + }, + { + name: "empty parties", + parties: pdz(), + exp: map[string]bool{}, + }, + { + name: "one party no signer", + parties: pdz(pd("addr1", "", "")), + exp: map[string]bool{}, + }, + { + name: "one party signer string", + parties: pdz(pd("addr1", "signer_string", "")), + exp: map[string]bool{addrStr("signer_string"): true}, + }, + { + name: "one party signer acc", + parties: pdz(pd("addr1", "", "signer_acc")), + exp: map[string]bool{addrStr("signer_acc"): true}, + }, + { + name: "one party both signer string and acc", + parties: pdz(pd("addr1", "signer_string", "signer_acc")), + exp: map[string]bool{addrStr("signer_string"): true}, + }, + { + name: "two parties neither have signer", + parties: pdz(pd("addr1", "", ""), pd("addr2", "", "")), + exp: map[string]bool{}, + }, + { + name: "two parties 1st has signer", + parties: pdz(pd("addr1", "signer1", ""), pd("addr2", "", "")), + exp: map[string]bool{addrStr("signer1"): true}, + }, + { + name: "two parties 2nd has signer", + parties: pdz(pd("addr1", "", ""), pd("addr2", "signer2", "")), + exp: map[string]bool{addrStr("signer2"): true}, + }, + { + name: "two parties both have different signer", + parties: pdz(pd("addr1", "signer1", ""), pd("addr2", "signer2", "")), + exp: map[string]bool{addrStr("signer1"): true, addrStr("signer2"): true}, + }, + { + name: "two parties both have same signer", + parties: pdz(pd("addr1", "signer1", ""), pd("addr2", "signer1", "")), + exp: map[string]bool{addrStr("signer1"): true}, + }, + { + name: "five parties, 1 without a signer, 1 with signer str, 1 with same signer acc, 2 with unique signers", + parties: pdz( + pd("addr1", "signer1", ""), + pd("addr2", "", ""), + pd("addr3", "", "signer2"), + pd("addr4", "", "signer1"), + pd("addr5", "signer3", ""), + ), + exp: map[string]bool{ + addrStr("signer1"): true, + addrStr("signer2"): true, + addrStr("signer3"): true, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := GetUsedSigners(tc.parties) + assert.Equal(t, tc.exp, actual, "GetAllSigners") + }) + } +} + +func TestNewTestablePartyDetails(t *testing.T) { + t.Run("nil panics", func(t *testing.T) { + expPanic := "runtime error: invalid memory address or nil pointer dereference" + testFunc := func() { + _ = NewTestablePartyDetails(nil) + } + assertions.RequirePanicEquals(t, testFunc, expPanic, "NewTestablePartyDetails") + }) + + t.Run("normal", func(t *testing.T) { + expected := TestablePartyDetails{ + Address: "address", + Role: 10, + Optional: true, + Acc: sdk.AccAddress("acc"), + Signer: "signer", + SignerAcc: sdk.AccAddress("signer_acc"), + CanBeUsedBySpec: true, + UsedBySpec: true, + } + pd := &PartyDetails{ + address: "address", + role: 10, + optional: true, + acc: sdk.AccAddress("acc"), + signer: "signer", + signerAcc: sdk.AccAddress("signer_acc"), + canBeUsedBySpec: true, + usedBySpec: true, + } + var actual TestablePartyDetails + testFunc := func() { + actual = NewTestablePartyDetails(pd) + } + require.NotPanics(t, testFunc, "NewTestablePartyDetails") + assert.Equal(t, expected, actual, "result of NewTestablePartyDetails") + assert.NotSame(t, pd.acc, actual.Acc, "the acc field") + actual.Acc[0] = actual.Acc[0] + 1 + assert.NotEqual(t, pd.acc, actual.Acc, "the acc field after a change to it in the result") + assert.NotSame(t, pd.signerAcc, actual.SignerAcc, "the signerAcc field") + actual.SignerAcc[0] = actual.SignerAcc[0] + 1 + assert.NotEqual(t, pd.signerAcc, actual.SignerAcc, "the signerAcc field after a change to it in the result") + }) +} + +func TestUsedSignersMap(t *testing.T) { + tests := []struct { + name string + actual UsedSignersMap + expected UsedSignersMap + isUsed []string + }{ + { + name: "NewUsedSignersMap", + actual: NewUsedSignersMap(), + expected: UsedSignersMap{}, + }, + { + name: "Use with two different addrs", + actual: NewUsedSignersMap().Use("addr1", "addr2"), + expected: UsedSignersMap{"addr1": true, "addr2": true}, + isUsed: []string{"addr1", "addr2"}, + }, + { + name: "Use with two same addrs", + actual: NewUsedSignersMap().Use("addr", "addr"), + expected: UsedSignersMap{"addr": true}, + isUsed: []string{"addr"}, + }, + { + name: "Use without any addrs", + actual: NewUsedSignersMap().Use(), + expected: UsedSignersMap{}, + }, + { + name: "Use twice different addrs", + actual: NewUsedSignersMap().Use("addr1").Use("addr2"), + expected: UsedSignersMap{"addr1": true, "addr2": true}, + isUsed: []string{"addr1", "addr2"}, + }, + { + name: "Use twice same addr", + actual: NewUsedSignersMap().Use("addr").Use("addr"), + expected: UsedSignersMap{"addr": true}, + isUsed: []string{"addr"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.actual) + for _, addr := range tc.isUsed { + isUsed := tc.actual.IsUsed(addr) + assert.True(t, isUsed, "IsUsed(%q)", addr) + } + }) + } +} + +func TestUsedSignersMap_AlsoUse(t *testing.T) { + tests := []struct { + name string + base UsedSignersMap + m2 UsedSignersMap + exp UsedSignersMap + }{ + { + name: "two different addrs", + base: NewUsedSignersMap().Use("addr1"), + m2: NewUsedSignersMap().Use("addr2"), + exp: UsedSignersMap{"addr1": true, "addr2": true}, + }, + { + name: "two same addrs", + base: NewUsedSignersMap().Use("addr"), + m2: NewUsedSignersMap().Use("addr"), + exp: UsedSignersMap{"addr": true}, + }, + { + name: "both empty", + base: NewUsedSignersMap(), + m2: NewUsedSignersMap(), + exp: UsedSignersMap{}, + }, + { + name: "base empty", + base: NewUsedSignersMap(), + m2: NewUsedSignersMap().Use("addr"), + exp: UsedSignersMap{"addr": true}, + }, + { + name: "m2 empty", + base: NewUsedSignersMap().Use("addr"), + m2: NewUsedSignersMap(), + exp: UsedSignersMap{"addr": true}, + }, + { + name: "m2 nil", + base: NewUsedSignersMap().Use("addr"), + m2: nil, + exp: UsedSignersMap{"addr": true}, + }, + { + name: "each have 3 with 1 common", + base: NewUsedSignersMap().Use("addr1", "addr2", "addr3"), + m2: NewUsedSignersMap().Use("addr3", "addr4", "addr5"), + exp: UsedSignersMap{ + "addr1": true, + "addr2": true, + "addr3": true, + "addr4": true, + "addr5": true, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual UsedSignersMap + testFunc := func() { + actual = tc.base.AlsoUse(tc.m2) + } + require.NotPanics(t, testFunc, "AlsoUse") + assert.Equal(t, tc.exp, actual, "AlsoUse return value") + assert.Equal(t, tc.exp, tc.base, "base after AlsoUse") + }) + } +} + +func TestAuthzCacheAcceptableKey(t *testing.T) { + grantee := sdk.AccAddress("y_grantee_z") + granter := sdk.AccAddress("Y_GRANTER_Z") + msgTypeURL := "1_msg_type_url_9" + + firstChar := func(str string) string { + return str[0:1] + } + lastChar := func(str string) string { + return str[len(str)-1:] + } + + tests := []struct { + name string + subStr string + contains bool + }{ + { + name: "grantee", + subStr: string(grantee), + contains: true, + }, + { + name: "granter", + subStr: string(granter), + contains: true, + }, + { + name: "msgTypeURL", + subStr: msgTypeURL, + contains: true, + }, + { + name: "grantee last granter first", + subStr: lastChar(string(grantee)) + firstChar(string(granter)), + contains: false, + }, + { + name: "granter last grantee first", + subStr: lastChar(string(granter)) + firstChar(string(grantee)), + contains: false, + }, + { + name: "grantee last msgTypeURL first", + subStr: lastChar(string(grantee)) + firstChar(msgTypeURL), + contains: false, + }, + { + name: "msgTypeURL last grantee first", + subStr: lastChar(msgTypeURL) + firstChar(string(grantee)), + contains: false, + }, + { + name: "granter last msgTypeURL first", + subStr: lastChar(string(granter)) + firstChar(msgTypeURL), + contains: false, + }, + { + name: "msgTypeURL last granter first", + subStr: lastChar(msgTypeURL) + firstChar(string(granter)), + contains: false, + }, + } + + actual := authzCacheAcceptableKey(grantee, granter, msgTypeURL) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.contains { + assert.Contains(t, actual, tc.subStr, "expected substring of authzCacheAcceptableKey result") + } else { + assert.NotContains(t, actual, tc.subStr, "unexpected substring of authzCacheAcceptableKey result") + } + }) + } +} + +func TestAuthzCacheIsWasmKey(t *testing.T) { + tests := []struct { + name string + str string + }{ + {name: "20 char addr", str: "20_character_address"}, + {name: "32 char addr", str: "thirty_two___character___address"}, + {name: "a space", str: " "}, + {name: "empty", str: ""}, + {name: "bytes 0 to 10", str: string([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10})}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + addr := sdk.AccAddress(tc.str) + actual := authzCacheIsWasmKey(addr) + assert.Equal(t, tc.str, actual, "authzCacheIsWasmKey") + }) + } +} + +func TestNewAuthzCache(t *testing.T) { + c1 := NewAuthzCache() + c1Type := fmt.Sprintf("%T", c1) + c2 := NewAuthzCache() + + assert.NotNil(t, c1, "NewAuthzCache result") + assert.Equal(t, "*types.AuthzCache", c1Type, "type returned by NewAuthzCache") + assert.Empty(t, c1.acceptable, "acceptable map") + assert.Empty(t, c1.isWasm, "isWasm map") + + assert.NotSame(t, c1, c2, "NewAuthzCache twice") + assert.NotSame(t, c1.acceptable, c2.acceptable, "acceptable maps of two NewAuthzCache") + assert.NotSame(t, c1.isWasm, c2.isWasm, "isWasm maps of two NewAuthzCache") +} + +func TestAuthzCache_Clear(t *testing.T) { + c := NewAuthzCache() + c.acceptable["key1"] = &authz.CountAuthorization{} + c.acceptable["key2"] = &authz.GenericAuthorization{} + c.isWasm["key3"] = true + c.isWasm["key4"] = false + assert.NotEmpty(t, c.acceptable, "AuthzCache acceptable map before clear") + assert.NotEmpty(t, c.isWasm, "AuthzCache isWasm map before clear") + c.Clear() + assert.Empty(t, c.acceptable, "AuthzCache acceptable map after clear") + assert.Empty(t, c.isWasm, "AuthzCache isWasm map after clear") +} + +func TestAuthzCache_SetAcceptable(t *testing.T) { + c := NewAuthzCache() + grantee := sdk.AccAddress("grantee") + granter := sdk.AccAddress("granter") + msgTypeURL := "msgTypeURL" + authorization := &authz.CountAuthorization{ + Msg: msgTypeURL, + AllowedAuthorizations: 77, + } + + c.SetAcceptable(grantee, granter, msgTypeURL, authorization) + actual := c.acceptable[authzCacheAcceptableKey(grantee, granter, msgTypeURL)] + assert.Equal(t, authorization, actual, "the authorization stored by SetAcceptable") +} + +func TestAuthzCache_GetAcceptable(t *testing.T) { + c := NewAuthzCache() + grantee := sdk.AccAddress("grantee") + granter := sdk.AccAddress("granter") + msgTypeURL := "msgTypeURL" + key := authzCacheAcceptableKey(grantee, granter, msgTypeURL) + + authorization := &authz.CountAuthorization{ + Msg: msgTypeURL, + AllowedAuthorizations: 8, + } + c.acceptable[key] = authorization + + actual := c.GetAcceptable(grantee, granter, msgTypeURL) + assert.Equal(t, authorization, actual, "GetAcceptable result") + + notThere := c.GetAcceptable(granter, grantee, msgTypeURL) + assert.Nil(t, notThere, "GetAcceptable on an entry that should not exist") +} + +func TestAuthzCache_SetIsWasm(t *testing.T) { + c := NewAuthzCache() + + // These tests will build on eachother using the same AuthzCache. + tests := []struct { + name string + addr sdk.AccAddress + value bool + exp map[string]bool + }{ + { + name: "new entry true", + addr: sdk.AccAddress("addr_true"), + value: true, + exp: map[string]bool{"addr_true": true}, + }, + { + name: "new entry false", + addr: sdk.AccAddress("addr_false"), + value: false, + exp: map[string]bool{"addr_true": true, "addr_false": false}, + }, + { + name: "change true to false", + addr: sdk.AccAddress("addr_true"), + value: false, + exp: map[string]bool{"addr_true": false, "addr_false": false}, + }, + { + name: "change false to true", + addr: sdk.AccAddress("addr_false"), + value: true, + exp: map[string]bool{"addr_true": false, "addr_false": true}, + }, + { + name: "nil address", + addr: nil, + value: true, + exp: map[string]bool{"addr_true": false, "addr_false": true, "": true}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + c.SetIsWasm(tc.addr, tc.value) + m := c.isWasm + assert.Equal(t, tc.exp, m, "isWasm map after SetIsWasm") + }) + } +} + +func TestAuthzCache_HasIsWasm(t *testing.T) { + c := NewAuthzCache() + addrTrue := sdk.AccAddress("addrTrue") + addrFalse := sdk.AccAddress("addrFalse") + addrUnknown := sdk.AccAddress("addrUnknown") + c.SetIsWasm(addrTrue, true) + c.SetIsWasm(addrFalse, false) + + tests := []struct { + name string + addr sdk.AccAddress + exp bool + }{ + {name: "known true", addr: addrTrue, exp: true}, + {name: "known false", addr: addrFalse, exp: true}, + {name: "unknown", addr: addrUnknown, exp: false}, + {name: "nil", addr: nil, exp: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := c.HasIsWasm(tc.addr) + assert.Equal(t, tc.exp, actual, "HasIsWasm") + }) + } +} + +func TestAuthzCache_GetIsWasm(t *testing.T) { + c := NewAuthzCache() + addrTrue := sdk.AccAddress("addrTrue") + addrFalse := sdk.AccAddress("addrFalse") + addrUnknown := sdk.AccAddress("addrUnknown") + c.SetIsWasm(addrTrue, true) + c.SetIsWasm(addrFalse, false) + + tests := []struct { + name string + addr sdk.AccAddress + exp bool + }{ + {name: "known true", addr: addrTrue, exp: true}, + {name: "known false", addr: addrFalse, exp: false}, + {name: "unknown", addr: addrUnknown, exp: false}, + {name: "nil", addr: nil, exp: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := c.GetIsWasm(tc.addr) + assert.Equal(t, tc.exp, actual, "GetIsWasm") + }) + } +} + +func TestAuthzCache_GetAcceptableMap(t *testing.T) { + makeCache := func(counts ...int32) *AuthzCache { + rv := NewAuthzCache() + for i, count := range counts { + rv.acceptable[fmt.Sprintf("key_%d__%d", i, count)] = &authz.CountAuthorization{ + Msg: fmt.Sprintf("msgTypeURL%d", i), + AllowedAuthorizations: count, + } + } + return rv + } + + tests := []struct { + name string + cache *AuthzCache + }{ + { + name: "nil", + cache: nil, + }, + { + name: "nil map", + cache: &AuthzCache{}, + }, + { + name: "empty map", + cache: makeCache(), + }, + { + name: "one entry", + cache: makeCache(5), + }, + { + name: "three entries", + cache: makeCache(52, 1, 12), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var expected map[string]authz.Authorization + if tc.cache != nil { + expected = tc.cache.acceptable + } + var actual map[string]authz.Authorization + testFunc := func() { + actual = tc.cache.GetAcceptableMap() + } + require.NotPanics(t, testFunc, "GetAcceptableMap") + if expected != nil && actual != nil { + require.NotSame(t, tc.cache.acceptable, actual, "result from GetAcceptableMap") + } + require.Equal(t, expected, actual, "result from GetAcceptableMap") + if len(actual) > 0 { + key := "" + for k := range actual { + if len(key) == 0 || k < key { + key = k + } + } + actual[key] = &authz.GenericAuthorization{Msg: "changed"} + assert.NotEqual(t, tc.cache.acceptable, actual, "after change to result from GetAcceptableMap") + } + }) + } +} + +func TestAuthzCache_GetIsWasmMap(t *testing.T) { + makeCache := func(bools ...bool) *AuthzCache { + rv := NewAuthzCache() + for i, b := range bools { + rv.isWasm[fmt.Sprintf("key_%d__%t", i, b)] = b + } + return rv + } + + tests := []struct { + name string + cache *AuthzCache + }{ + { + name: "nil", + cache: nil, + }, + { + name: "nil map", + cache: &AuthzCache{}, + }, + { + name: "empty map", + cache: makeCache(), + }, + { + name: "one true entry", + cache: makeCache(true), + }, + { + name: "one false entry", + cache: makeCache(false), + }, + { + name: "three entries: true true true", + cache: makeCache(true, true, true), + }, + { + name: "three entries: true true false", + cache: makeCache(true, true, false), + }, + { + name: "three entries: true false true", + cache: makeCache(true, false, true), + }, + { + name: "three entries: false true true", + cache: makeCache(false, true, true), + }, + { + name: "three entries: true false false", + cache: makeCache(true, false, false), + }, + { + name: "three entries: false true false", + cache: makeCache(false, true, false), + }, + { + name: "three entries: false false true", + cache: makeCache(false, false, true), + }, + { + name: "three entries: false false false", + cache: makeCache(false, false, false), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var expected map[string]bool + if tc.cache != nil { + expected = tc.cache.isWasm + } + var actual map[string]bool + testFunc := func() { + actual = tc.cache.GetIsWasmMap() + } + require.NotPanics(t, testFunc, "GetIsWasmMap") + if expected != nil && actual != nil { + require.NotSame(t, tc.cache.isWasm, actual, "result from GetIsWasmMap") + } + require.Equal(t, expected, actual, "result from GetIsWasmMap") + if len(actual) > 0 { + key := "" + for k := range actual { + if len(key) == 0 || k < key { + key = k + } + } + actual[key] = !actual[key] + assert.NotEqual(t, tc.cache.isWasm, actual, "after change to result from GetIsWasmMap") + } + }) + } +} + +func TestAddAuthzCacheToContext(t *testing.T) { + t.Run("context does not already have the key", func(t *testing.T) { + origCtx := emptySdkContext() + newCtx := AddAuthzCacheToContext(origCtx) + + cacheOrig := origCtx.Value(authzCacheContextKey) + assert.Nil(t, cacheOrig, "original context %q value", authzCacheContextKey) + + cacheV := newCtx.Value(authzCacheContextKey) + require.NotNil(t, cacheV, "new context %q value", authzCacheContextKey) + cache, ok := cacheV.(*AuthzCache) + require.True(t, ok, "can cast %q value to *AuthzCache", authzCacheContextKey) + require.NotNil(t, cache, "the %q value cast to a *AuthzCache", authzCacheContextKey) + assert.Empty(t, cache.acceptable, "the acceptable map of the newly added *AuthzCache") + }) + + t.Run("context already has an AuthzCache", func(t *testing.T) { + grantee := sdk.AccAddress("grantee") + granter := sdk.AccAddress("granter") + msgTypeURL := "msgTypeURL" + authorization := &authz.CountAuthorization{ + Msg: msgTypeURL, + AllowedAuthorizations: 8, + } + origCache := NewAuthzCache() + origCache.SetAcceptable(grantee, granter, msgTypeURL, authorization) + + origCtx := emptySdkContext().WithValue(authzCacheContextKey, origCache) + newCtx := AddAuthzCacheToContext(origCtx) + + var newCache *AuthzCache + testFunc := func() { + newCache = GetAuthzCache(newCtx) + } + require.NotPanics(t, testFunc, "GetAuthzCache") + assert.Same(t, origCache, newCache, "cache from new context") + assert.Empty(t, newCache.acceptable, "cache acceptable map") + }) + + t.Run("context has something else", func(t *testing.T) { + origCtx := emptySdkContext().WithValue(authzCacheContextKey, "something else") + + expErr := "context value \"authzCacheContextKey\" is a string, expected *types.AuthzCache" + testFunc := func() { + _ = AddAuthzCacheToContext(origCtx) + } + require.PanicsWithError(t, expErr, testFunc, "AddAuthzCacheToContext") + }) +} + +func TestGetAuthzCache(t *testing.T) { + t.Run("context does not have it", func(t *testing.T) { + ctx := emptySdkContext() + expErr := "context does not contain a \"authzCacheContextKey\" value" + testFunc := func() { + _ = GetAuthzCache(ctx) + } + require.PanicsWithError(t, expErr, testFunc, "GetAuthzCache") + }) + + t.Run("context has something else", func(t *testing.T) { + ctx := emptySdkContext().WithValue(authzCacheContextKey, "something else") + expErr := "context value \"authzCacheContextKey\" is a string, expected *types.AuthzCache" + testFunc := func() { + _ = GetAuthzCache(ctx) + } + require.PanicsWithError(t, expErr, testFunc, "GetAuthzCache") + }) + + t.Run("context has it", func(t *testing.T) { + origCache := NewAuthzCache() + origCache.acceptable["key1"] = &authz.GenericAuthorization{Msg: "msg"} + ctx := emptySdkContext().WithValue(authzCacheContextKey, origCache) + var cache *AuthzCache + testFunc := func() { + cache = GetAuthzCache(ctx) + } + require.NotPanics(t, testFunc, "GetAuthzCache") + assert.Same(t, origCache, cache, "cache returned by GetAuthzCache") + }) +} diff --git a/x/quarantine/genesis.pb.go b/x/quarantine/genesis.pb.go index 34930db173..98a7952bd5 100644 --- a/x/quarantine/genesis.pb.go +++ b/x/quarantine/genesis.pb.go @@ -6,7 +6,6 @@ package quarantine import ( fmt "fmt" _ "github.com/cosmos/cosmos-proto" - _ "github.com/cosmos/gogoproto/gogoproto" proto "github.com/cosmos/gogoproto/proto" io "io" math "math" @@ -97,27 +96,26 @@ func init() { } var fileDescriptor_1a60633c09654351 = []byte{ - // 308 bytes of a gzipped FileDescriptorProto + // 297 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4f, 0xce, 0x2f, 0xce, 0xcd, 0x2f, 0xd6, 0x2f, 0x2c, 0x4d, 0x2c, 0x4a, 0xcc, 0x2b, 0xc9, 0xcc, 0x4b, 0xd5, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, 0x34, 0xd4, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x92, 0x84, 0x28, 0xd4, 0x43, 0x28, 0xd4, 0x83, 0x2a, 0x94, 0xd2, 0xc2, - 0x6d, 0x06, 0x92, 0x6a, 0xb0, 0x31, 0x52, 0x50, 0x63, 0xe2, 0xc1, 0x3c, 0x7d, 0xa8, 0x99, 0x10, - 0x29, 0x91, 0xf4, 0xfc, 0xf4, 0x7c, 0x88, 0x38, 0x88, 0x05, 0x11, 0x55, 0xea, 0x61, 0xe2, 0xe2, - 0x71, 0x87, 0xb8, 0x24, 0xb8, 0x24, 0xb1, 0x24, 0x55, 0xc8, 0x97, 0x4b, 0x14, 0x61, 0x6a, 0x4a, - 0x7c, 0x62, 0x4a, 0x4a, 0x51, 0x6a, 0x71, 0x71, 0x6a, 0xb1, 0x04, 0xa3, 0x02, 0xb3, 0x06, 0xa7, - 0x93, 0xc4, 0xa5, 0x2d, 0xba, 0x22, 0x50, 0x73, 0x1d, 0x21, 0x72, 0xc1, 0x25, 0x45, 0x99, 0x79, - 0xe9, 0x41, 0x22, 0x48, 0xda, 0x1c, 0x61, 0xba, 0x84, 0x82, 0xb9, 0xf8, 0x12, 0x4b, 0x4b, 0xf2, - 0xe3, 0x8b, 0x52, 0x8b, 0x0b, 0xf2, 0xf3, 0x40, 0xe6, 0x30, 0x29, 0x30, 0x6b, 0x70, 0x1b, 0xe9, - 0xe8, 0xe1, 0xf4, 0xb0, 0x9e, 0x63, 0x69, 0x49, 0x7e, 0x10, 0x54, 0xbd, 0x6b, 0x5e, 0x49, 0x51, - 0x65, 0x10, 0x6f, 0x22, 0x92, 0x50, 0xb1, 0x50, 0x04, 0x97, 0x20, 0xb2, 0x1b, 0xd3, 0x4a, 0xf3, - 0x52, 0x8a, 0x25, 0x98, 0xc1, 0xe6, 0x6a, 0xe3, 0x31, 0x37, 0x10, 0xa1, 0xc7, 0x0d, 0xa4, 0x25, - 0x48, 0xa0, 0x10, 0x4d, 0xc4, 0xc9, 0xeb, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, - 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0xe1, 0xc2, 0x63, 0x39, 0x86, 0x1b, 0x8f, 0xe5, 0x18, - 0xa2, 0x0c, 0xd2, 0x33, 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0x0b, 0x8a, 0xf2, - 0xcb, 0x52, 0xf3, 0x12, 0xf3, 0x92, 0x53, 0x75, 0x33, 0xf3, 0x91, 0x78, 0xfa, 0x15, 0x48, 0x31, - 0x92, 0xc4, 0x06, 0x0e, 0x61, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0x81, 0xc5, 0xad, 0x9d, - 0x04, 0x02, 0x00, 0x00, + 0x6d, 0x06, 0x92, 0x6a, 0xb0, 0x31, 0x52, 0x50, 0x63, 0xe2, 0xc1, 0x3c, 0x7d, 0xa8, 0x99, 0x60, + 0x8e, 0x52, 0x0f, 0x13, 0x17, 0x8f, 0x3b, 0xc4, 0xce, 0xe0, 0x92, 0xc4, 0x92, 0x54, 0x21, 0x5f, + 0x2e, 0x51, 0x84, 0xfe, 0x94, 0xf8, 0xc4, 0x94, 0x94, 0xa2, 0xd4, 0xe2, 0xe2, 0xd4, 0x62, 0x09, + 0x46, 0x05, 0x66, 0x0d, 0x4e, 0x27, 0x89, 0x4b, 0x5b, 0x74, 0x45, 0xa0, 0x26, 0x38, 0x42, 0xe4, + 0x82, 0x4b, 0x8a, 0x32, 0xf3, 0xd2, 0x83, 0x44, 0x90, 0xb4, 0x39, 0xc2, 0x74, 0x09, 0x05, 0x73, + 0xf1, 0x25, 0x96, 0x96, 0xe4, 0xc7, 0x17, 0xa5, 0x16, 0x17, 0xe4, 0xe7, 0x81, 0xcc, 0x61, 0x52, + 0x60, 0xd6, 0xe0, 0x36, 0xd2, 0xd1, 0xc3, 0xe9, 0x35, 0x3d, 0xc7, 0xd2, 0x92, 0xfc, 0x20, 0xa8, + 0x7a, 0xd7, 0xbc, 0x92, 0xa2, 0xca, 0x20, 0xde, 0x44, 0x24, 0xa1, 0x62, 0xa1, 0x08, 0x2e, 0x41, + 0x64, 0x37, 0xa6, 0x95, 0xe6, 0xa5, 0x14, 0x4b, 0x30, 0x83, 0xcd, 0xd5, 0xc6, 0x63, 0x6e, 0x20, + 0x42, 0x8f, 0x1b, 0x48, 0x4b, 0x90, 0x40, 0x21, 0x9a, 0x88, 0x93, 0xd7, 0x89, 0x47, 0x72, 0x8c, + 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72, + 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0x19, 0xa4, 0x67, 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, + 0xe7, 0xea, 0x17, 0x14, 0xe5, 0x97, 0xa5, 0xe6, 0x25, 0xe6, 0x25, 0xa7, 0xea, 0x66, 0xe6, 0x23, + 0xf1, 0xf4, 0x2b, 0x90, 0xc2, 0x3e, 0x89, 0x0d, 0x1c, 0xc2, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, + 0xff, 0xfd, 0xad, 0xe4, 0xe9, 0xee, 0x01, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { diff --git a/x/quarantine/tx.pb.go b/x/quarantine/tx.pb.go index 1fd232cdb2..6c827cbe53 100644 --- a/x/quarantine/tx.pb.go +++ b/x/quarantine/tx.pb.go @@ -10,7 +10,6 @@ import ( github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/types" _ "github.com/cosmos/cosmos-sdk/types/msgservice" - _ "github.com/cosmos/cosmos-sdk/types/query" _ "github.com/cosmos/cosmos-sdk/types/tx/amino" _ "github.com/cosmos/gogoproto/gogoproto" grpc1 "github.com/cosmos/gogoproto/grpc" @@ -525,48 +524,47 @@ func init() { } var fileDescriptor_d2d4535ca5d9aa17 = []byte{ - // 647 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x55, 0x3f, 0x6f, 0xd3, 0x40, - 0x14, 0xcf, 0x11, 0xfa, 0x27, 0x57, 0x5a, 0xa8, 0x5b, 0x41, 0x6a, 0x21, 0x37, 0x0a, 0x20, 0x45, - 0x81, 0xd8, 0xa4, 0x0c, 0x08, 0x16, 0x94, 0x82, 0x40, 0x20, 0x45, 0x95, 0x0c, 0x1d, 0x40, 0x48, - 0xd1, 0xc5, 0xbe, 0x1a, 0x8b, 0xfa, 0xce, 0xf5, 0x9d, 0xab, 0x76, 0x43, 0xb0, 0x20, 0x26, 0x66, - 0x06, 0x66, 0x04, 0x4b, 0x87, 0xf2, 0x1d, 0x3a, 0x56, 0x4c, 0x4c, 0x80, 0xda, 0xa1, 0x5f, 0x03, - 0xd9, 0x77, 0xe7, 0x5a, 0x6d, 0x48, 0x42, 0xc5, 0xc2, 0x12, 0x9f, 0xdf, 0xfb, 0xfd, 0x79, 0x2f, - 0x7a, 0xcf, 0x07, 0xab, 0x0e, 0x65, 0x01, 0x65, 0xd6, 0x5a, 0x8c, 0x22, 0x44, 0xb8, 0x4f, 0xb0, - 0xb5, 0xde, 0xec, 0x62, 0x8e, 0x9a, 0x16, 0xdf, 0x30, 0xc3, 0x88, 0x72, 0xaa, 0xcd, 0x09, 0x8c, - 0x79, 0x88, 0x31, 0x25, 0x46, 0x9f, 0x46, 0x81, 0x4f, 0xa8, 0x95, 0xfe, 0x0a, 0xb4, 0x5e, 0x97, - 0x8a, 0x5d, 0xc4, 0xb0, 0xb5, 0x16, 0xe3, 0x68, 0x33, 0x53, 0x0c, 0x91, 0xe7, 0x13, 0xc4, 0x7d, - 0x4a, 0x24, 0xd6, 0xc8, 0x63, 0x15, 0xca, 0xa1, 0xbe, 0xca, 0x5f, 0x90, 0xf9, 0x80, 0x79, 0xd6, - 0x7a, 0x33, 0x79, 0x1c, 0x31, 0xe9, 0x51, 0x76, 0xae, 0x4a, 0x81, 0x95, 0xe5, 0x77, 0xd2, 0x37, - 0x4b, 0xf6, 0x22, 0x52, 0xb3, 0x1e, 0xf5, 0xa8, 0x88, 0x27, 0x27, 0x11, 0xad, 0x3e, 0x81, 0xe3, - 0x6d, 0xe6, 0x2d, 0x85, 0xfc, 0x21, 0xd1, 0x6e, 0x42, 0xc8, 0x69, 0x07, 0xb9, 0x6e, 0x84, 0x19, - 0x2b, 0x83, 0x0a, 0xa8, 0x95, 0x16, 0xcb, 0xdf, 0xb6, 0x1b, 0xb3, 0x52, 0xa7, 0x25, 0x32, 0x8f, - 0x79, 0xe4, 0x13, 0xcf, 0x2e, 0x71, 0x2a, 0x03, 0xb7, 0xcf, 0xbe, 0x3e, 0xd8, 0xaa, 0xe7, 0xb8, - 0x55, 0x0d, 0x9e, 0x53, 0xaa, 0x36, 0x66, 0x21, 0x25, 0x0c, 0x57, 0x97, 0x61, 0x49, 0xc4, 0x96, - 0x62, 0xfe, 0x0f, 0xad, 0x66, 0xe0, 0x74, 0x26, 0x9b, 0x79, 0x6d, 0x83, 0xd4, 0xac, 0xe5, 0x38, - 0x38, 0x3c, 0xb9, 0x99, 0x76, 0x07, 0x4e, 0xad, 0x44, 0x34, 0x50, 0x54, 0xcc, 0xca, 0xa7, 0x2a, - 0xc5, 0xbe, 0xe4, 0xc9, 0x04, 0xdf, 0x52, 0x70, 0xed, 0x22, 0x2c, 0x85, 0x38, 0x0a, 0x10, 0xc1, - 0x84, 0x97, 0x8b, 0x15, 0x50, 0x1b, 0xb7, 0x0f, 0x03, 0xc7, 0x7b, 0xf9, 0x08, 0xd2, 0x66, 0x44, - 0xd9, 0xaa, 0x19, 0xed, 0x2d, 0x80, 0x53, 0x2b, 0x31, 0x71, 0x59, 0x27, 0xc2, 0xab, 0x18, 0x31, - 0xec, 0x96, 0x41, 0xa5, 0x58, 0x9b, 0x58, 0x98, 0x33, 0x65, 0x0d, 0xc9, 0x48, 0xa9, 0x31, 0x35, - 0xef, 0x52, 0x9f, 0x2c, 0xde, 0xdf, 0xf9, 0x31, 0x5f, 0xf8, 0xfc, 0x73, 0xbe, 0xe6, 0xf9, 0xfc, - 0x45, 0xdc, 0x35, 0x1d, 0x1a, 0xc8, 0x69, 0x90, 0x8f, 0x06, 0x73, 0x5f, 0x5a, 0x7c, 0x33, 0xc4, - 0x2c, 0x25, 0xb0, 0x0f, 0x07, 0x5b, 0xf5, 0x33, 0xab, 0xd8, 0x43, 0xce, 0x66, 0x27, 0x19, 0x4a, - 0xf6, 0xe9, 0x60, 0xab, 0x0e, 0xec, 0xc9, 0xd4, 0xd8, 0x96, 0xbe, 0xd5, 0xaf, 0x00, 0xc2, 0x36, - 0xf3, 0xee, 0x61, 0x67, 0xd5, 0x27, 0xf8, 0xff, 0xf9, 0x63, 0x67, 0xa1, 0x76, 0x58, 0x76, 0x36, - 0x25, 0x5f, 0x00, 0x3c, 0xdf, 0x66, 0xde, 0x72, 0xe8, 0x22, 0x8e, 0x5b, 0x31, 0xa7, 0x2a, 0xc3, - 0x4e, 0xde, 0xd9, 0x03, 0x38, 0x16, 0xa7, 0x7a, 0xa2, 0xa5, 0x89, 0x85, 0x86, 0xf9, 0xc7, 0x2f, - 0x8a, 0x99, 0xf7, 0x14, 0x55, 0xd8, 0x8a, 0x7d, 0xbc, 0x87, 0x0a, 0x34, 0x7a, 0x17, 0xab, 0x0e, - 0x0b, 0xef, 0x4e, 0xc3, 0x62, 0x9b, 0x79, 0xda, 0x53, 0x38, 0x22, 0x16, 0xfa, 0x52, 0x1f, 0x6f, - 0xb5, 0x9f, 0xfa, 0xd5, 0x21, 0x40, 0xd9, 0x2c, 0x3e, 0x87, 0xa3, 0x72, 0x83, 0x2f, 0x0f, 0xa4, - 0x2d, 0xc5, 0x5c, 0xbf, 0x36, 0x0c, 0x2a, 0xaf, 0x2e, 0x57, 0x76, 0x80, 0xba, 0x40, 0x0d, 0x52, - 0x3f, 0xb2, 0x47, 0x1d, 0x38, 0xa6, 0x06, 0xf7, 0x4a, 0x7f, 0xa2, 0x84, 0xe9, 0x8d, 0xa1, 0x60, - 0x99, 0xc1, 0x1b, 0x00, 0x67, 0x7a, 0x0d, 0x53, 0xb3, 0xbf, 0x4c, 0x0f, 0x8a, 0x7e, 0xeb, 0xaf, - 0x29, 0xea, 0xa0, 0x8f, 0xbc, 0x4a, 0x36, 0x77, 0xf1, 0xd1, 0xce, 0x9e, 0x01, 0x76, 0xf7, 0x0c, - 0xf0, 0x6b, 0xcf, 0x00, 0xef, 0xf7, 0x8d, 0xc2, 0xee, 0xbe, 0x51, 0xf8, 0xbe, 0x6f, 0x14, 0x9e, - 0x5d, 0xcf, 0x7d, 0x13, 0xc2, 0x88, 0xae, 0x63, 0x82, 0x88, 0x83, 0x1b, 0x3e, 0xcd, 0xbd, 0x59, - 0x1b, 0xb9, 0xbb, 0xa5, 0x3b, 0x9a, 0xde, 0x15, 0x37, 0x7e, 0x07, 0x00, 0x00, 0xff, 0xff, 0xf1, - 0xc7, 0xe4, 0x22, 0x41, 0x07, 0x00, 0x00, + // 631 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x55, 0xbf, 0x6f, 0xd3, 0x4e, + 0x14, 0xcf, 0x7d, 0xf3, 0xed, 0x8f, 0x5c, 0x69, 0xa1, 0x6e, 0x04, 0xa9, 0x85, 0xdc, 0x28, 0x80, + 0x14, 0x05, 0x62, 0x93, 0x30, 0x20, 0x58, 0x50, 0x02, 0x02, 0x81, 0x14, 0x45, 0x32, 0x74, 0x00, + 0x21, 0x45, 0x8e, 0x7d, 0x35, 0x16, 0xf1, 0x9d, 0xf1, 0x9d, 0xa3, 0x76, 0x43, 0xb0, 0x20, 0x26, + 0x66, 0x06, 0x66, 0x04, 0x4b, 0x86, 0xf2, 0x3f, 0x74, 0xac, 0x98, 0x98, 0x00, 0x25, 0x43, 0xfe, + 0x0d, 0x64, 0xdf, 0x39, 0xb1, 0x68, 0x9a, 0x84, 0x8a, 0x85, 0x25, 0x3e, 0xbf, 0xf7, 0xf9, 0xf1, + 0x5e, 0xf4, 0x9e, 0x0f, 0x16, 0x4c, 0x42, 0x5d, 0x42, 0xb5, 0x17, 0x81, 0xe1, 0x1b, 0x98, 0x39, + 0x18, 0x69, 0xdd, 0x4a, 0x1b, 0x31, 0xa3, 0xa2, 0xb1, 0x5d, 0xd5, 0xf3, 0x09, 0x23, 0xd2, 0x26, + 0xc7, 0xa8, 0x63, 0x8c, 0x2a, 0x30, 0xf2, 0xba, 0xe1, 0x3a, 0x98, 0x68, 0xd1, 0x2f, 0x47, 0xcb, + 0x8a, 0x50, 0x6c, 0x1b, 0x74, 0xac, 0x65, 0x12, 0x07, 0x8b, 0xfc, 0x39, 0x91, 0x77, 0xa9, 0xad, + 0x75, 0x2b, 0xe1, 0x43, 0x24, 0x4a, 0xc7, 0x97, 0x92, 0x70, 0xe6, 0x58, 0x51, 0x52, 0x2b, 0x7a, + 0xd3, 0x44, 0x7d, 0x3c, 0x95, 0xb5, 0x89, 0x4d, 0x78, 0x3c, 0x3c, 0xf1, 0x68, 0xe1, 0x11, 0x5c, + 0x6e, 0x50, 0xbb, 0xe9, 0xb1, 0xfb, 0x58, 0xba, 0x0e, 0x21, 0x23, 0x2d, 0xc3, 0xb2, 0x7c, 0x44, + 0x69, 0x0e, 0xe4, 0x41, 0x31, 0x53, 0xcf, 0x7d, 0xdd, 0x2f, 0x67, 0x85, 0x4e, 0x8d, 0x67, 0x1e, + 0x32, 0xdf, 0xc1, 0xb6, 0x9e, 0x61, 0x44, 0x04, 0x6e, 0x9e, 0x7e, 0x35, 0xec, 0x95, 0x12, 0xdc, + 0x82, 0x04, 0xcf, 0xc4, 0xaa, 0x3a, 0xa2, 0x1e, 0xc1, 0x14, 0x15, 0xb6, 0x61, 0x86, 0xc7, 0x9a, + 0x01, 0xfb, 0x8b, 0x56, 0x1b, 0x70, 0x7d, 0x24, 0x3b, 0xf2, 0xda, 0x07, 0x91, 0x59, 0xcd, 0x34, + 0x91, 0x77, 0x72, 0x33, 0xe9, 0x16, 0x5c, 0xdb, 0xf1, 0x89, 0x1b, 0x53, 0x11, 0xcd, 0xfd, 0x97, + 0x4f, 0x4f, 0x25, 0xaf, 0x86, 0xf8, 0x5a, 0x0c, 0x97, 0xce, 0xc3, 0x8c, 0x87, 0x7c, 0xd7, 0xc0, + 0x08, 0xb3, 0x5c, 0x3a, 0x0f, 0x8a, 0xcb, 0xfa, 0x38, 0x70, 0xb4, 0x97, 0x0f, 0x20, 0x6a, 0x86, + 0x97, 0x1d, 0x37, 0x23, 0xbd, 0x01, 0x70, 0x6d, 0x27, 0xc0, 0x16, 0x6d, 0xf9, 0xa8, 0x83, 0x0c, + 0x8a, 0xac, 0x1c, 0xc8, 0xa7, 0x8b, 0x2b, 0xd5, 0x4d, 0x55, 0xd4, 0x10, 0x8e, 0x54, 0x3c, 0x7a, + 0xea, 0x6d, 0xe2, 0xe0, 0xfa, 0xdd, 0x83, 0xef, 0x5b, 0xa9, 0x4f, 0x3f, 0xb6, 0x8a, 0xb6, 0xc3, + 0x9e, 0x05, 0x6d, 0xd5, 0x24, 0xae, 0x98, 0x06, 0xf1, 0x28, 0x53, 0xeb, 0xb9, 0xc6, 0xf6, 0x3c, + 0x44, 0x23, 0x02, 0x7d, 0x3f, 0xec, 0x95, 0x4e, 0x75, 0x90, 0x6d, 0x98, 0x7b, 0xad, 0x70, 0x28, + 0xe9, 0xc7, 0x61, 0xaf, 0x04, 0xf4, 0xd5, 0xc8, 0x58, 0x17, 0xbe, 0x85, 0x2f, 0x00, 0xc2, 0x06, + 0xb5, 0xef, 0x20, 0xb3, 0xe3, 0x60, 0xf4, 0xef, 0xfc, 0xb1, 0x59, 0x28, 0x8d, 0xcb, 0x1e, 0x4d, + 0xc9, 0x67, 0x00, 0xcf, 0x36, 0xa8, 0xbd, 0xed, 0x59, 0x06, 0x43, 0xb5, 0x80, 0x91, 0x38, 0x43, + 0x4f, 0xde, 0xd9, 0x3d, 0xb8, 0x14, 0x44, 0x7a, 0xbc, 0xa5, 0x95, 0x6a, 0x59, 0x3d, 0xf6, 0x2b, + 0xa1, 0x26, 0x3d, 0x79, 0x15, 0x7a, 0xcc, 0x3e, 0xda, 0x43, 0x1e, 0x2a, 0x93, 0x8b, 0x8d, 0x0f, + 0xd5, 0xb7, 0xff, 0xc3, 0x74, 0x83, 0xda, 0xd2, 0x63, 0xb8, 0xc0, 0x17, 0xfa, 0xc2, 0x14, 0xef, + 0x78, 0x3f, 0xe5, 0xcb, 0x73, 0x80, 0x46, 0xb3, 0xf8, 0x14, 0x2e, 0x8a, 0x0d, 0xbe, 0x38, 0x93, + 0xd6, 0x0c, 0x98, 0x7c, 0x65, 0x1e, 0x54, 0x52, 0x5d, 0xac, 0xec, 0x0c, 0x75, 0x8e, 0x9a, 0xa5, + 0xfe, 0xdb, 0x1e, 0xb5, 0xe0, 0x52, 0x3c, 0xb8, 0x97, 0xa6, 0x13, 0x05, 0x4c, 0x2e, 0xcf, 0x05, + 0x1b, 0x19, 0xbc, 0x06, 0x70, 0x63, 0xd2, 0x30, 0x55, 0xa6, 0xcb, 0x4c, 0xa0, 0xc8, 0x37, 0xfe, + 0x98, 0x12, 0x1f, 0xe4, 0x85, 0x97, 0xe1, 0xe6, 0xd6, 0x1f, 0x1c, 0xf4, 0x15, 0x70, 0xd8, 0x57, + 0xc0, 0xcf, 0xbe, 0x02, 0xde, 0x0d, 0x94, 0xd4, 0xe1, 0x40, 0x49, 0x7d, 0x1b, 0x28, 0xa9, 0x27, + 0x57, 0x13, 0xdf, 0x04, 0xcf, 0x27, 0x5d, 0x84, 0x0d, 0x6c, 0xa2, 0xb2, 0x43, 0x12, 0x6f, 0xda, + 0x6e, 0xe2, 0x6e, 0x69, 0x2f, 0x46, 0x77, 0xc5, 0xb5, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x5a, + 0xd4, 0x9c, 0x3f, 0x15, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used.