Repository: desmos-labs/desmos
Pull request #494 (2021-06-16 14:20:37)

Case insensitive DTag
<!-- < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < ☺
v                               ✰  Thanks for creating a PR! ✰    
v    Before smashing the submit button please review the checkboxes.
v    If a checkbox is n/a - please still include it but + a little note why
☺ > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >  -->

## Description
This PR makes the `profiles` DTag case insensitive.
#492.
 
## Checklist
- [x] Targeted PR against correct branch.
- [x] Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
- [ ] Wrote unit tests.
- [ ] Wrote integration tests (simulation & CLI). 
- [ ] Updated the documentation. 
- [ ] Added an entry to the `CHANGELOG.md` file.
- [ ] Re-reviewed `Files changed` in the Github PR explorer.


RiccardoM: @bragaz Sims tests are failing, better check out why 
bragaz: @RiccardoM I've added two more tests as you asked to ensure the fact that DTag casing is working properly and check also the error of the sim tests debugging it:
![Schermata 2021-06-18 alle 10 03 20](https://user-images.githubusercontent.com/26248202/122528569-c2c0a980-d01c-11eb-9aa9-ec2bc76c4c06.png)

Considering the error I think it's worth to add that check on `StoreProfile` right?
RiccardoM: > Considering the error I think it's worth to add that check on `StoreProfile` right?

I think this error started showing up now because case insensitive DTags actually work. If we take a look at the test code, what we do is: 
1. We get the user address and see if it has a profile 
2. If it does not have a profile, we store a new one with a random DTag

Previously, if the random DTag did not match only because of casing, nothing would happen. But now, since we have case insensitive DTags, this means that if the random DTag is equals to the one of another profile that error will show up. 

For this reason, I think we should review how simulation tests deal with profiles. Instead of getting a random recipient, and then creating a profile for such recipient, what we can do instead is getting a random profile. This way we do not have to generate a new one cause we will always use an already existing one. The only thing we need to check is to make sure that the recipient and message signer are not the same user. 
RiccardoM: @bragaz Can you please add a CHANGELOG entry since this change is going to break the consensus state please? Then we're ready to merge this
bragaz: @RiccardoM done

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b99d5b7c..bd3d292b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 - Changed how chain links are stored ([#480](https://github.com/desmos-labs/desmos/issues/480))
 - Added the ability to paginate the user poll answers ([#478](https://github.com/desmos-labs/desmos/issues/478))
 - Added the logging of some interesting operations ([#483](https://github.com/desmos-labs/desmos/issues/483))
+- Make `x/profiles` DTag case-insensitive ([#492](https://github.com/desmos-labs/desmos/issues/492))
 
 # Version 0.16.3
 ## Changes
diff --git a/x/profiles/keeper/keeper_ibc.go b/x/profiles/keeper/keeper_ibc.go
index ce16a583..93eb8ea3 100644
--- a/x/profiles/keeper/keeper_ibc.go
+++ b/x/profiles/keeper/keeper_ibc.go
@@ -6,19 +6,12 @@ import (
 	capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
 	channeltypes "github.com/cosmos/cosmos-sdk/x/ibc/core/04-channel/types"
 	host "github.com/cosmos/cosmos-sdk/x/ibc/core/24-host"
-	"github.com/tendermint/tendermint/libs/log"
-
 	"github.com/desmos-labs/desmos/x/profiles/types"
 )
 
 // DONTCOVER
 // No need to cover this simple methods
 
-// Logger returns a module-specific logger.
-func (k Keeper) Logger(ctx sdk.Context) log.Logger {
-	return ctx.Logger().With("module", "x/"+host.ModuleName+"-"+types.ModuleName)
-}
-
 // ChanCloseInit defines a wrapper function for the channel Keeper's function
 func (k Keeper) ChanCloseInit(ctx sdk.Context, portID, channelID string) error {
 	capName := host.ChannelCapabilityPath(portID, channelID)
diff --git a/x/profiles/simulation/operations_dtag_transfers.go b/x/profiles/simulation/operations_dtag_transfers.go
index f9ef7c01..6b32089a 100644
--- a/x/profiles/simulation/operations_dtag_transfers.go
+++ b/x/profiles/simulation/operations_dtag_transfers.go
@@ -29,7 +29,7 @@ func SimulateMsgRequestDTagTransfer(
 		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
 		accs []simtypes.Account, chainID string,
 	) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) {
-		sender, request, skip := randomDTagRequestTransferFields(r, ctx, accs, k, ak)
+		sender, request, skip := randomDTagRequestTransferFields(r, ctx, accs, k)
 		if skip {
 			return simtypes.NoOpMsg(types.RouterKey, types.ModuleName, ""), nil, nil
 		}
@@ -84,49 +84,37 @@ func sendMsgRequestDTagTransfer(
 
 // randomDTagRequestTransferFields returns random dTagRequest data
 func randomDTagRequestTransferFields(
-	r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, k keeper.Keeper, ak authkeeper.AccountKeeper,
+	r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, k keeper.Keeper,
 ) (simtypes.Account, types.DTagTransferRequest, bool) {
 	if len(accs) == 0 {
 		return simtypes.Account{}, types.DTagTransferRequest{}, true
 	}
 
-	// Get random accounts
-	receiver, _ := simtypes.RandomAcc(r, accs)
+	// Get random sender account
 	sender, _ := simtypes.RandomAcc(r, accs)
 
-	// Skip if the two addresses are equals
-	if receiver.Equals(sender) {
+	profiles := k.GetProfiles(ctx)
+	if profiles == nil {
 		return simtypes.Account{}, types.DTagTransferRequest{}, true
 	}
 
-	// Skip if the receiver is blocked
-	if k.IsUserBlocked(ctx, receiver.Address.String(), sender.Address.String()) {
+	// Get a random Profile
+	receiverProfile := RandomProfile(r, profiles)
+	receiverAddress := receiverProfile.GetAddress()
+	if receiverAddress.Equals(sender.Address) {
 		return simtypes.Account{}, types.DTagTransferRequest{}, true
 	}
 
-	randomDTag := RandomDTag(r)
-
-	// Get the current auth account of the receiver.
-	// If the receiver already has a profile, we need to first extract the auth account from it. This is done in order
-	// to avoid later storing a Profile that contains a Profile inside itself (which would cause later bugs in the
-	// export/import process)
-	account := ak.GetAccount(ctx, receiver.Address)
-	if customProfile, ok := account.(*types.Profile); ok {
-		account = customProfile.GetAccount()
-	}
-
-	// Create the receiver profile
-	receiverProfile, err := types.NewProfileFromAccount(randomDTag, account, ctx.BlockTime())
-	if err != nil {
+	// Skip if the sender is blocked
+	if k.IsUserBlocked(ctx, receiverAddress.String(), sender.Address.String()) {
 		return simtypes.Account{}, types.DTagTransferRequest{}, true
 	}
-	_ = k.StoreProfile(ctx, receiverProfile)
 
 	// Create a request
-	req := types.NewDTagTransferRequest(randomDTag, sender.Address.String(), receiver.Address.String())
+	req := types.NewDTagTransferRequest(receiverProfile.DTag, sender.Address.String(), receiverAddress.String())
 
 	// Skip if requests already exists
-	requests := k.GetUserIncomingDTagTransferRequests(ctx, receiver.Address.String())
+	requests := k.GetUserIncomingDTagTransferRequests(ctx, receiverAddress.String())
 	for _, request := range requests {
 		if request.Sender == req.Sender {
 			return simtypes.Account{}, types.DTagTransferRequest{}, true
diff --git a/x/profiles/types/keys.go b/x/profiles/types/keys.go
index dc3835f4..ef55428f 100644
--- a/x/profiles/types/keys.go
+++ b/x/profiles/types/keys.go
@@ -57,7 +57,7 @@ var (
 
 // DTagStoreKey turns a DTag into the key used to store the address associated with it into the store
 func DTagStoreKey(dTag string) []byte {
-	return append(DTagPrefix, []byte(dTag)...)
+	return append(DTagPrefix, []byte(strings.ToLower(dTag))...)
 }
 
 // DTagTransferRequestStoreKey turns an address to a key used to store a transfer request into the profiles store


Test patch:
diff --git a/x/profiles/keeper/msgs_server_profile_test.go b/x/profiles/keeper/msgs_server_profile_test.go
index eb927de7..a19cb3b5 100644
--- a/x/profiles/keeper/msgs_server_profile_test.go
+++ b/x/profiles/keeper/msgs_server_profile_test.go
@@ -99,6 +99,92 @@ func (suite *KeeperTestSuite) Test_handleMsgSaveProfile() {
 				)),
 			},
 		},
+		{
+			name:      "Profile saved with same DTag but capital first letter (with previous profile created)",
+			blockTime: suite.testData.profile.CreationDate,
+			existentProfiles: []*types.Profile{
+				suite.CheckProfileNoError(types.NewProfile(
+					"test",
+					"old-nickname",
+					"old-biography",
+					types.NewPictures(
+						"https://test.com/old-profile-pic",
+						"https://test.com/old-cover-pic",
+					),
+					suite.testData.profile.CreationDate,
+					suite.testData.profile.GetAccount(),
+				)),
+			},
+			msg: types.NewMsgSaveProfile(
+				"Test",
+				"nickname",
+				"biography",
+				"https://test.com/profile-pic",
+				"https://test.com/cover-pic",
+				suite.testData.profile.GetAddress().String(),
+			),
+			expEvents: sdk.Events{
+				sdk.NewEvent(
+					types.EventTypeProfileSaved,
+					sdk.NewAttribute(types.AttributeProfileDTag, "Test"),
+					sdk.NewAttribute(types.AttributeProfileCreator, suite.testData.profile.GetAddress().String()),
+					sdk.NewAttribute(types.AttributeProfileCreationTime, suite.testData.profile.CreationDate.Format(time.RFC3339)),
+				),
+			},
+			expStoredProfiles: []*types.Profile{
+				suite.CheckProfileNoError(types.NewProfile(
+					"Test",
+					"nickname",
+					"biography",
+					types.NewPictures(
+						"https://test.com/profile-pic",
+						"https://test.com/cover-pic",
+					),
+					suite.testData.profile.CreationDate,
+					suite.testData.profile.GetAccount(),
+				)),
+			},
+		},
+		{
+			name:      "Profile not saved because of the same DTag",
+			blockTime: suite.testData.profile.CreationDate,
+			existentProfiles: []*types.Profile{
+				suite.CheckProfileNoError(types.NewProfile(
+					"test",
+					"nickname",
+					"biography",
+					types.NewPictures(
+						"https://test.com/profile-pic",
+						"https://test.com/cover-pic",
+					),
+					suite.testData.profile.CreationDate,
+					suite.testData.profile.GetAccount(),
+				)),
+			},
+			msg: types.NewMsgSaveProfile(
+				"Test",
+				"another-one",
+				"biography",
+				"https://test.com/profile-pic",
+				"https://test.com/cover-pic",
+				suite.testData.otherUser,
+			),
+			expEvents: sdk.EmptyEvents(),
+			shouldErr: true,
+			expStoredProfiles: []*types.Profile{
+				suite.CheckProfileNoError(types.NewProfile(
+					"test",
+					"nickname",
+					"biography",
+					types.NewPictures(
+						"https://test.com/profile-pic",
+						"https://test.com/cover-pic",
+					),
+					suite.testData.profile.CreationDate,
+					suite.testData.profile.GetAccount(),
+				)),
+			},
+		},
 		{
 			name:      "Profile not edited because of the invalid profile picture",
 			blockTime: suite.testData.profile.CreationDate,