Repository: desmos-labs/desmos
Pull request #514 (2021-06-24 06:53:02)

Chains and application links improvements
<!-- < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < ☺
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 fixes some bugs that I found while manually testing the Desmos <-> Band integration. 
It also implements some improvements that will be useful for third party tools like DJuno. 

## Checklist
- [ ] Targeted PR against correct branch.
- [ ] 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.


dadamu: @RiccardoM Just found some error when testing because the codec of `KeeperTestSuite` in  `commen_test.go` is still `codec.BinaryMarshaler`. It should be change to `codec.Marshaler`, too.
codecov[bot]: # [Codecov](https://codecov.io/gh/desmos-labs/desmos/pull/514?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs) Report
> Merging [#514](https://codecov.io/gh/desmos-labs/desmos/pull/514?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs) (4b0b47a) into [master](https://codecov.io/gh/desmos-labs/desmos/commit/504d24e823bc7e1abce47de938f3c3e2cb59c310?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs) (504d24e) will **decrease** coverage by `0.29%`.
> The diff coverage is `47.05%`.

> :exclamation: Current head 4b0b47a differs from pull request most recent head 8514c68. Consider uploading reports for the commit 8514c68 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/desmos-labs/desmos/pull/514/graphs/tree.svg?width=650&height=150&src=pr&token=c87ulmWL91&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs)](https://codecov.io/gh/desmos-labs/desmos/pull/514?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs)

```diff
@@            Coverage Diff             @@
##           master     #514      +/-   ##
==========================================
- Coverage   84.94%   84.64%   -0.30%     
==========================================
  Files          89       89              
  Lines        4284     4318      +34     
==========================================
+ Hits         3639     3655      +16     
- Misses        484      501      +17     
- Partials      161      162       +1     
```


| [Impacted Files](https://codecov.io/gh/desmos-labs/desmos/pull/514?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs) | Coverage Δ | |
|---|---|---|
| [x/profiles/keeper/msg\_server\_app\_link.go](https://codecov.io/gh/desmos-labs/desmos/pull/514/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs#diff-eC9wcm9maWxlcy9rZWVwZXIvbXNnX3NlcnZlcl9hcHBfbGluay5nbw==) | `0.00% <0.00%> (ø)` | |
| [x/profiles/types/models\_app\_links.go](https://codecov.io/gh/desmos-labs/desmos/pull/514/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs#diff-eC9wcm9maWxlcy90eXBlcy9tb2RlbHNfYXBwX2xpbmtzLmdv) | `94.44% <0.00%> (-1.07%)` | :arrow_down: |
| [x/profiles/types/query\_app\_links.go](https://codecov.io/gh/desmos-labs/desmos/pull/514/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs#diff-eC9wcm9maWxlcy90eXBlcy9xdWVyeV9hcHBfbGlua3MuZ28=) | `0.00% <0.00%> (ø)` | |
| [x/profiles/keeper/grpc\_query.go](https://codecov.io/gh/desmos-labs/desmos/pull/514/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs#diff-eC9wcm9maWxlcy9rZWVwZXIvZ3JwY19xdWVyeS5nbw==) | `76.34% <84.21%> (+2.01%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/desmos-labs/desmos/pull/514?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/desmos-labs/desmos/pull/514?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs). Last update [504d24e...8514c68](https://codecov.io/gh/desmos-labs/desmos/pull/514?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=desmos-labs).


Patch:
diff --git a/proto/desmos/posts/v1beta1/query.proto b/proto/desmos/posts/v1beta1/query.proto
index 8e8a2d54..0ad671b7 100644
--- a/proto/desmos/posts/v1beta1/query.proto
+++ b/proto/desmos/posts/v1beta1/query.proto
@@ -53,8 +53,8 @@ service Query {
 
 // QueryPostsRequest is the request type for the Query/Posts RPC method.
 message QueryPostsRequest {
-  // SubspaceId represents the ID of the subspace to which to query the posts for.
-  // Providing an empty or invalid subspace id will return an error.
+  // SubspaceId represents the ID of the subspace to which to query the posts
+  // for. Providing an empty or invalid subspace id will return an error.
   string subspace_id = 1;
 
   // pagination defines an optional pagination for the request.
diff --git a/proto/desmos/profiles/v1beta1/query.proto b/proto/desmos/profiles/v1beta1/query.proto
index 9c45c812..c3e37eed 100644
--- a/proto/desmos/profiles/v1beta1/query.proto
+++ b/proto/desmos/profiles/v1beta1/query.proto
@@ -57,9 +57,33 @@ service Query {
         "/desmos/profiles/v1beta1/chain-links/{user}";
   }
 
+  // UserChainLink queries the chain link for the given user, chain name and
+  // target address
+  rpc UserChainLink(QueryUserChainLinkRequest)
+      returns (QueryUserChainLinkResponse) {
+    option (google.api.http).get =
+        "/desmos/profiles/v1beta1/chain-links/{user}/{chain_name}/{target}";
+  }
+
   // UserApplicationLinks queries application links for the given user
   rpc UserApplicationLinks(QueryUserApplicationLinksRequest)
       returns (QueryUserApplicationLinksResponse) {
     option (google.api.http).get = "/desmos/profiles/v1beta1/app-links/{user}";
   }
+
+  // UserApplicationLinks queries a single application link for a given user,
+  // searching via the application name and username
+  rpc UserApplicationLink(QueryUserApplicationLinkRequest)
+      returns (QueryUserApplicationLinkResponse) {
+    option (google.api.http).get =
+        "/desmos/profiles/v1beta1/app-links/{user}/{application}/{username}";
+  }
+
+  // ApplicationLinkByClientID queries a single application link for a given
+  // client id.
+  rpc ApplicationLinkByClientID(QueryApplicationLinkByClientIDRequest)
+      returns (QueryApplicationLinkByClientIDResponse) {
+    option (google.api.http).get =
+        "/desmos/profiles/v1beta1/app-links/clients/{client_id}";
+  }
 }
diff --git a/proto/desmos/profiles/v1beta1/query_app_links.proto b/proto/desmos/profiles/v1beta1/query_app_links.proto
index 6c06f44a..b7c961e4 100644
--- a/proto/desmos/profiles/v1beta1/query_app_links.proto
+++ b/proto/desmos/profiles/v1beta1/query_app_links.proto
@@ -10,8 +10,28 @@ import "cosmos/base/query/v1beta1/pagination.proto";
 
 option go_package = "github.com/desmos-labs/desmos/x/profiles/types";
 
-// QueryUserApplicationLinksRequest represent the request used when querying the
-// application links of a specific user
+// QueryUserApplicationLinkRequest represents the request used when querying an
+// application link using an application name and username for a given user
+message QueryUserApplicationLinkRequest {
+  // User contains the Desmos profile address associated for which the link
+  // should be searched for
+  string user = 1;
+  // Application represents the application name associated with the link
+  string application = 2;
+  // Username represents the username inside the application associated with the
+  // link
+  string username = 3;
+}
+
+// QueryUserApplicationLinkResponse represents the response to the query
+// allowing to get an application link for a specific user, searching via the
+// application name and username
+message QueryUserApplicationLinkResponse {
+  ApplicationLink link = 1 [ (gogoproto.nullable) = false ];
+}
+
+// QueryUserApplicationLinksRequest represents the request used when querying
+// the application links of a specific user
 message QueryUserApplicationLinksRequest {
   string user = 1;
 
@@ -27,3 +47,16 @@ message QueryUserApplicationLinksResponse {
   // Pagination defines the pagination response
   cosmos.base.query.v1beta1.PageResponse pagination = 2;
 }
+
+// QueryApplicationLinkByClientIDRequest contains the data of the request that
+// can be used to get an application link based on a client id
+message QueryApplicationLinkByClientIDRequest {
+  // ClientID represents the ID of the client to which search the link for
+  string client_id = 1;
+}
+
+// QueryApplicationLinkByClientIDResponse contains the data returned by the
+// request allowing to get an application link using a client id
+message QueryApplicationLinkByClientIDResponse {
+  ApplicationLink link = 1 [ (gogoproto.nullable) = false ];
+}
\ No newline at end of file
diff --git a/proto/desmos/profiles/v1beta1/query_chain_links.proto b/proto/desmos/profiles/v1beta1/query_chain_links.proto
index b0b4a802..9d68fb75 100644
--- a/proto/desmos/profiles/v1beta1/query_chain_links.proto
+++ b/proto/desmos/profiles/v1beta1/query_chain_links.proto
@@ -10,6 +10,24 @@ import "cosmos/base/query/v1beta1/pagination.proto";
 
 option go_package = "github.com/desmos-labs/desmos/x/profiles/types";
 
+// QueryUserChainLinkRequest represents the request that should be used in order
+// to retrieve the link associated with the provided user, for the given chain
+// and having the given target address
+message QueryUserChainLinkRequest {
+  // User represents the Desmos address of the user to which search the link for
+  string user = 1;
+  // ChainName contains the name of the chain to which search the link for
+  string chain_name = 2;
+  // Target must contain the external address to which query the link for
+  string target = 3;
+}
+
+// QueryUserChainLinkResponse contains the data that is returned when querying a
+// specific chain link
+message QueryUserChainLinkResponse {
+  ChainLink link = 1 [ (gogoproto.nullable) = false ];
+}
+
 // QueryUserChainLinksRequest is the request type for the
 // Query/UserChainLinks RPC endpoint
 message QueryUserChainLinksRequest {
diff --git a/x/profiles/handler.go b/x/profiles/handler.go
index ac4ddcbe..14cf4eef 100644
--- a/x/profiles/handler.go
+++ b/x/profiles/handler.go
@@ -64,6 +64,14 @@ func NewHandler(k keeper.Keeper) sdk.Handler {
 			res, err := msgServer.UnlinkChainAccount(sdk.WrapSDKContext(ctx), msg)
 			return sdk.WrapServiceResult(ctx, res, err)
 
+		case *types.MsgLinkApplication:
+			res, err := msgServer.LinkApplication(sdk.WrapSDKContext(ctx), msg)
+			return sdk.WrapServiceResult(ctx, res, err)
+
+		case *types.MsgUnlinkApplication:
+			res, err := msgServer.UnlinkApplication(sdk.WrapSDKContext(ctx), msg)
+			return sdk.WrapServiceResult(ctx, res, err)
+
 		default:
 			return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest,
 				"unrecognized %s message type: %v", types.ModuleName, msg.Type())
diff --git a/x/profiles/keeper/grpc_query.go b/x/profiles/keeper/grpc_query.go
index b7c634a7..7d4ae97f 100644
--- a/x/profiles/keeper/grpc_query.go
+++ b/x/profiles/keeper/grpc_query.go
@@ -140,6 +140,18 @@ func (k Keeper) UserChainLinks(ctx context.Context, request *types.QueryUserChai
 	return &types.QueryUserChainLinksResponse{Links: links, Pagination: pageRes}, nil
 }
 
+// UserChainLink implements the Query/UserChainLink gRPC method
+func (k Keeper) UserChainLink(ctx context.Context, request *types.QueryUserChainLinkRequest) (*types.QueryUserChainLinkResponse, error) {
+	sdkCtx := sdk.UnwrapSDKContext(ctx)
+
+	link, found := k.GetChainLink(sdkCtx, request.User, request.ChainName, request.Target)
+	if !found {
+		return nil, status.Error(codes.NotFound, "link not found")
+	}
+
+	return &types.QueryUserChainLinkResponse{Link: link}, nil
+}
+
 // UserApplicationLinks implements the Query/UserApplicationLinks gRPC method
 func (k Keeper) UserApplicationLinks(ctx context.Context, request *types.QueryUserApplicationLinksRequest) (*types.QueryUserApplicationLinksResponse, error) {
 	sdkCtx := sdk.UnwrapSDKContext(ctx)
@@ -168,3 +180,35 @@ func (k Keeper) UserApplicationLinks(ctx context.Context, request *types.QueryUs
 
 	return &types.QueryUserApplicationLinksResponse{Links: links, Pagination: pageRes}, nil
 }
+
+// UserApplicationLink implements the Query/UserApplicationLink gRPC method
+func (k Keeper) UserApplicationLink(ctx context.Context, request *types.QueryUserApplicationLinkRequest) (*types.QueryUserApplicationLinkResponse, error) {
+	sdkCtx := sdk.UnwrapSDKContext(ctx)
+
+	link, found, err := k.GetApplicationLink(sdkCtx, request.User, request.Application, request.Username)
+	if err != nil {
+		return nil, status.Error(codes.Internal, err.Error())
+	}
+
+	if !found {
+		return nil, status.Errorf(codes.NotFound, "link not found")
+	}
+
+	return &types.QueryUserApplicationLinkResponse{Link: link}, nil
+}
+
+// ApplicationLinkByClientID implements the Query/ApplicationLinkByClientID gRPC method
+func (k Keeper) ApplicationLinkByClientID(ctx context.Context, request *types.QueryApplicationLinkByClientIDRequest) (*types.QueryApplicationLinkByClientIDResponse, error) {
+	sdkCtx := sdk.UnwrapSDKContext(ctx)
+
+	link, err := k.GetApplicationLinkByClientID(sdkCtx, request.ClientId)
+	if err != nil {
+		if sdkerrors.ErrNotFound.Is(err) {
+			return nil, status.Errorf(codes.NotFound, "link for client id %s not found", request.ClientId)
+		}
+
+		return nil, status.Error(codes.Internal, err.Error())
+	}
+
+	return &types.QueryApplicationLinkByClientIDResponse{Link: link}, nil
+}
diff --git a/x/profiles/keeper/msg_server_app_link.go b/x/profiles/keeper/msg_server_app_link.go
index bed51338..348fd112 100644
--- a/x/profiles/keeper/msg_server_app_link.go
+++ b/x/profiles/keeper/msg_server_app_link.go
@@ -2,9 +2,12 @@ package keeper
 
 import (
 	"context"
+	"time"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
 
+	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+
 	"github.com/desmos-labs/desmos/x/profiles/types"
 )
 
@@ -19,6 +22,18 @@ func (k Keeper) LinkApplication(
 		return nil, err
 	}
 
+	// See if the link already exists
+	link, found, err := k.GetApplicationLink(ctx, msg.Sender, msg.LinkData.Application, msg.LinkData.Username)
+	if err != nil {
+		return nil, err
+	}
+
+	if found && link.IsVerificationOngoing() {
+		return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest,
+			"verification process of link for application %s and username %s is already happening",
+			msg.LinkData.Application, msg.LinkData.Username)
+	}
+
 	if err := k.StartProfileConnection(
 		ctx, msg.LinkData, msg.CallData, user,
 		msg.SourcePort, msg.SourceChannel, msg.TimeoutHeight, msg.TimeoutTimestamp,
@@ -37,6 +52,7 @@ func (k Keeper) LinkApplication(
 			sdk.NewAttribute(types.AttributeKeyUser, msg.Sender),
 			sdk.NewAttribute(types.AttributeKeyApplicationName, msg.LinkData.Application),
 			sdk.NewAttribute(types.AttributeKeyApplicationUsername, msg.LinkData.Username),
+			sdk.NewAttribute(types.AttributeKeyApplicationLinkCreationTime, ctx.BlockTime().Format(time.RFC3339)),
 		),
 		sdk.NewEvent(
 			sdk.EventTypeMessage,
diff --git a/x/profiles/module_ibc.go b/x/profiles/module_ibc.go
index 4b345402..cfdbb310 100644
--- a/x/profiles/module_ibc.go
+++ b/x/profiles/module_ibc.go
@@ -186,35 +186,37 @@ func (am AppModule) OnRecvPacket(
 	}, ackBytes, nil
 }
 
-// PacketHandler represents a method that tries handling a packet, and returns types.ErrInvalidPacketData
-// if it cannot handle the contained data
-type PacketHandler = func(am AppModule, ctx sdk.Context, packet channeltypes.Packet) (channeltypes.Acknowledgement, error)
+// PacketHandler represents a method that tries handling a packet.
+// If the packet has been handled properly, handled will be true and the acknowledgment and error will
+// tell how the execution has ended.
+// If the packet cannot be handled properly, false will be returned instead as first value.
+type PacketHandler = func(
+	am AppModule, ctx sdk.Context, packet channeltypes.Packet,
+) (handled bool, ack channeltypes.Acknowledgement, err error)
 
-// HandlePacket handles the given packet by passing it to the provided packet handlers one by one until either one can
-// handle it, or they all return with types.ErrInvalidPacketData.
+// HandlePacket handles the given packet by passing it to the provided packet handlers one by one until
+// at least one of them can handle it.
+// If no handler supports the given packet, it returns types.ErrInvalidPacketData.
 func (am AppModule) HandlePacket(
 	ctx sdk.Context, packet channeltypes.Packet, packetHandlers ...PacketHandler,
 ) (channeltypes.Acknowledgement, error) {
-	var ack channeltypes.Acknowledgement
-	var err error
 	for _, handler := range packetHandlers {
-		ack, err = handler(am, ctx, packet)
-		if types.ErrInvalidPacketData.Is(err) {
-			continue
+		handled, ack, err := handler(am, ctx, packet)
+		if handled {
+			return ack, err
 		}
 	}
-	return ack, err
+	return channeltypes.Acknowledgement{}, sdkerrors.Wrapf(types.ErrInvalidPacketData, "%T", packet)
 }
 
 // handleLinkChainAccountPacketData tries handling athe given packet by deserializing the inner data
 // as a LinkChainAccountPacketData instance.
-// Returns ErrInvalidPacketData if the given packet data is not of such type.
 func handleLinkChainAccountPacketData(
 	am AppModule, ctx sdk.Context, packet channeltypes.Packet,
-) (channeltypes.Acknowledgement, error) {
+) (handled bool, ack channeltypes.Acknowledgement, err error) {
 	var packetData types.LinkChainAccountPacketData
 	if err := types.ModuleCdc.UnmarshalJSON(packet.GetData(), &packetData); err != nil {
-		return channeltypes.Acknowledgement{}, sdkerrors.Wrapf(types.ErrInvalidPacketData, "%T", packet)
+		return false, channeltypes.Acknowledgement{}, nil
 	}
 
 	var acknowledgement channeltypes.Acknowledgement
@@ -226,14 +228,14 @@ func handleLinkChainAccountPacketData(
 		// Encode packet acknowledgment
 		packetAckBytes, err := packetAck.Marshal()
 		if err != nil {
-			return channeltypes.Acknowledgement{}, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
+			return true, channeltypes.Acknowledgement{}, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
 		}
 		acknowledgement = channeltypes.NewResultAcknowledgement(packetAckBytes)
 	}
 
 	address, err := types.UnpackAddressData(am.cdc, packetData.SourceAddress)
 	if err != nil {
-		return channeltypes.Acknowledgement{}, err
+		return true, channeltypes.Acknowledgement{}, err
 	}
 
 	ctx.EventManager().EmitEvent(
@@ -247,23 +249,22 @@ func handleLinkChainAccountPacketData(
 		),
 	)
 
-	return acknowledgement, nil
+	return true, acknowledgement, nil
 }
 
 // handleOracleRequestPacketData tries handling athe given packet by deserializing the inner data
 // as an OracleResponsePacketData instance.
-// Returns ErrInvalidPacketData if the given packet data is not of such type.
 func handleOracleRequestPacketData(
 	am AppModule, ctx sdk.Context, packet channeltypes.Packet,
-) (channeltypes.Acknowledgement, error) {
+) (handled bool, ack channeltypes.Acknowledgement, err error) {
 	var data oracletypes.OracleResponsePacketData
 	if err := types.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
-		return channeltypes.Acknowledgement{}, sdkerrors.Wrapf(types.ErrInvalidPacketData, "%T", packet)
+		return false, channeltypes.Acknowledgement{}, nil
 	}
 
 	acknowledgement := channeltypes.NewResultAcknowledgement([]byte{byte(1)})
 
-	err := am.keeper.OnRecvApplicationLinkPacketData(ctx, data)
+	err = am.keeper.OnRecvApplicationLinkPacketData(ctx, data)
 	if err != nil {
 		acknowledgement = channeltypes.NewErrorAcknowledgement(err.Error())
 	}
@@ -280,7 +281,7 @@ func handleOracleRequestPacketData(
 	)
 
 	// NOTE: acknowledgement will be written synchronously during IBC handler execution.
-	return acknowledgement, nil
+	return true, acknowledgement, nil
 }
 
 // -------------------------------------------------------------------------------------------------------------------
diff --git a/x/profiles/types/events.go b/x/profiles/types/events.go
index d5fb1af4..92eefd82 100644
--- a/x/profiles/types/events.go
+++ b/x/profiles/types/events.go
@@ -49,14 +49,15 @@ const (
 	EventTypesApplicationLinkSaved   = "application_link_saved"
 	EventTypeApplicationLinkDeleted  = "application_link_deleted"
 
-	AttributeKeyUser                = "user"
-	AttributeKeyApplicationName     = "application_name"
-	AttributeKeyApplicationUsername = "application_username"
-	AttributeKeyOracleID            = "oracle_id"
-	AttributeKeyClientID            = "client_id"
-	AttributeKeyRequestID           = "request_id"
-	AttributeKeyRequestKey          = "request_key"
-	AttributeKeyResolveStatus       = "resolve_status"
-	AttributeKeyAck                 = "acknowledgement"
-	AttributeKeyAckError            = "error"
+	AttributeKeyUser                        = "user"
+	AttributeKeyApplicationName             = "application_name"
+	AttributeKeyApplicationUsername         = "application_username"
+	AttributeKeyApplicationLinkCreationTime = "application_link_creation_time"
+	AttributeKeyOracleID                    = "oracle_id"
+	AttributeKeyClientID                    = "client_id"
+	AttributeKeyRequestID                   = "request_id"
+	AttributeKeyRequestKey                  = "request_key"
+	AttributeKeyResolveStatus               = "resolve_status"
+	AttributeKeyAck                         = "acknowledgement"
+	AttributeKeyAckError                    = "error"
 )
diff --git a/x/profiles/types/keys.go b/x/profiles/types/keys.go
index ef55428f..4038596d 100644
--- a/x/profiles/types/keys.go
+++ b/x/profiles/types/keys.go
@@ -36,8 +36,10 @@ const (
 
 	DoNotModify = "[do-not-modify]"
 
-	// IBC keys
-	IBCVersion = "ibc-profiles-1"
+	// IBCVersion represents the version of the IBC specification to be used.
+	// Currently we use ICS20-1 because we need to communicate with Band which uses the same version.
+	// TODO: Explore if it's possible to support multiple specifications at once, and see if we should create our own one.
+	IBCVersion = "ics20-1"
 	IBCPortID  = "ibc-profiles"
 )
 
@@ -48,7 +50,6 @@ var (
 	UsersBlocksStorePrefix        = []byte("users_blocks")
 	ChainLinksPrefix              = []byte("chain_links")
 	UserApplicationLinkPrefix     = []byte("user_application_link")
-	ApplicationLinkPrefix         = []byte("application_link")
 	ApplicationLinkClientIDPrefix = []byte("client_id")
 
 	// IBCPortKey defines the key to store the port ID in store
diff --git a/x/profiles/types/models_app_links.go b/x/profiles/types/models_app_links.go
index 46b46e17..bfb3f665 100644
--- a/x/profiles/types/models_app_links.go
+++ b/x/profiles/types/models_app_links.go
@@ -55,6 +55,11 @@ func (l ApplicationLink) Validate() error {
 	return nil
 }
 
+// IsVerificationOngoing tells whether the verification for the link is still ongoing
+func (l *ApplicationLink) IsVerificationOngoing() bool {
+	return l.State == ApplicationLinkStateInitialized || l.State == AppLinkStateVerificationStarted
+}
+
 // MustMarshalApplicationLink serializes the given application link using the provided BinaryMarshaler
 func MustMarshalApplicationLink(cdc codec.BinaryMarshaler, link ApplicationLink) []byte {
 	return cdc.MustMarshalBinaryBare(&link)
diff --git a/x/profiles/types/query.pb.go b/x/profiles/types/query.pb.go
index 3c3e01dd..eaf6fa57 100644
--- a/x/profiles/types/query.pb.go
+++ b/x/profiles/types/query.pb.go
@@ -35,43 +35,51 @@ func init() {
 }
 
 var fileDescriptor_5e0074f57a59f38d = []byte{
-	// 572 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0x3f, 0x6f, 0xd3, 0x40,
-	0x18, 0x87, 0x73, 0x48, 0x14, 0x71, 0x12, 0x48, 0x9c, 0x2a, 0xa1, 0x46, 0xc8, 0x08, 0x18, 0x4a,
-	0x9b, 0xda, 0xd7, 0x24, 0x15, 0x88, 0x6e, 0x14, 0x06, 0x06, 0x86, 0x52, 0x95, 0x85, 0x25, 0x3a,
-	0xbb, 0x17, 0xc7, 0xaa, 0xe3, 0xbb, 0xde, 0x9d, 0x11, 0x15, 0x62, 0x61, 0x66, 0x40, 0x62, 0xe1,
-	0x03, 0xb0, 0xb0, 0xf1, 0x05, 0x90, 0x3a, 0x32, 0x56, 0x62, 0x61, 0x44, 0x09, 0x1f, 0x04, 0xf9,
-	0xfe, 0xa4, 0x98, 0xca, 0x76, 0xd2, 0x2d, 0x76, 0x9e, 0xdf, 0xfb, 0x3e, 0xf7, 0xfa, 0xee, 0xe0,
-	0xbd, 0x03, 0x2a, 0xc7, 0x4c, 0x62, 0x2e, 0xd8, 0x30, 0x49, 0xa9, 0xc4, 0xaf, 0xbb, 0x21, 0x55,
-	0xa4, 0x8b, 0x8f, 0x72, 0x2a, 0x8e, 0x03, 0x2e, 0x98, 0x62, 0xe8, 0xa6, 0x81, 0x02, 0x07, 0x05,
-	0x16, 0x6a, 0x2f, 0xc7, 0x2c, 0x66, 0x9a, 0xc1, 0xc5, 0x2f, 0x83, 0xb7, 0x6f, 0xc5, 0x8c, 0xc5,
-	0x29, 0xc5, 0x84, 0x27, 0x98, 0x64, 0x19, 0x53, 0x44, 0x25, 0x2c, 0x93, 0xf6, 0xdf, 0x15, 0xfb,
-	0xaf, 0x7e, 0x0a, 0xf3, 0x21, 0x26, 0x99, 0xed, 0xd3, 0xee, 0xd4, 0xca, 0x0c, 0xec, 0x6b, 0x0b,
-	0x77, 0xeb, 0x61, 0x41, 0x53, 0xd3, 0x76, 0x94, 0x70, 0x39, 0x5f, 0xe4, 0x40, 0x91, 0x78, 0x20,
-	0xe8, 0x51, 0x4e, 0xa5, 0x72, 0x91, 0xf5, 0x06, 0x25, 0x22, 0xc8, 0xd8, 0xb1, 0xb8, 0x9e, 0x8d,
-	0x46, 0x24, 0xc9, 0x06, 0x69, 0x92, 0x1d, 0xba, 0x80, 0x5f, 0x1f, 0x20, 0x9c, 0x97, 0xf0, 0x95,
-	0x88, 0x15, 0xf8, 0xc0, 0x0c, 0xdc, 0x3c, 0x38, 0x4d, 0xf3, 0x84, 0x43, 0x22, 0xa9, 0x49, 0xcf,
-	0x6a, 0x71, 0x12, 0x27, 0x99, 0x1e, 0x85, 0x61, 0x7b, 0xdf, 0xaf, 0xc2, 0xcb, 0x2f, 0x0a, 0x04,
-	0x7d, 0x06, 0xf0, 0xca, 0xae, 0xe9, 0x8d, 0x36, 0x82, 0x8a, 0x8f, 0x1c, 0x68, 0xd6, 0x62, 0x7b,
-	0x66, 0x3a, 0x6d, 0x7f, 0x4e, 0x5a, 0x72, 0x96, 0x49, 0x7a, 0x77, 0xf3, 0xfd, 0xcf, 0x3f, 0x9f,
-	0x2e, 0xad, 0xa3, 0xfb, 0x95, 0x23, 0x9a, 0xbd, 0x78, 0x9b, 0x4b, 0x2a, 0xde, 0xa1, 0xaf, 0x00,
-	0x5e, 0x7b, 0xba, 0x4f, 0xe2, 0x7d, 0x41, 0x32, 0x39, 0xa4, 0x42, 0xa2, 0x5e, 0x7d, 0xcb, 0x12,
-	0xec, 0x34, 0xfb, 0x0b, 0x65, 0xac, 0x2c, 0xd6, 0xb2, 0x6b, 0x68, 0xb5, 0x52, 0x56, 0x6f, 0x14,
-	0x35, 0x33, 0xfb, 0x00, 0xe0, 0xd2, 0xae, 0xde, 0x08, 0xa8, 0xd3, 0x30, 0x17, 0x4d, 0x39, 0xbb,
-	0x8d, 0xf9, 0x60, 0xab, 0xb5, 0xaa, 0xb5, 0xee, 0xa0, 0xdb, 0xd5, 0x33, 0x34, 0x0e, 0x27, 0x00,
-	0xde, 0x78, 0x29, 0xa9, 0xd8, 0xfb, 0xf7, 0x04, 0xa0, 0x07, 0xf5, 0xcd, 0xce, 0x05, 0x9c, 0xe4,
-	0xc3, 0x85, 0x73, 0xd6, 0x77, 0x5b, 0xfb, 0x6e, 0xa1, 0x9e, 0xf3, 0x2d, 0x9d, 0xc8, 0x99, 0x74,
-	0xf9, 0xad, 0xfd, 0xfa, 0x5f, 0x00, 0x84, 0x45, 0xe5, 0x9d, 0x94, 0x45, 0x87, 0x12, 0xe1, 0x66,
-	0x07, 0x43, 0x3a, 0xe9, 0xcd, 0xf9, 0x03, 0xd6, 0xb6, 0xaf, 0x6d, 0x7d, 0xd4, 0xa9, 0xb7, 0x0d,
-	0x75, 0xca, 0x69, 0x7e, 0x03, 0xf0, 0x7a, 0x51, 0xeb, 0x49, 0x71, 0xb2, 0x9f, 0x17, 0x27, 0x15,
-	0xf5, 0x9b, 0x3b, 0x9f, 0xd1, 0x4e, 0x77, 0x6b, 0xb1, 0x50, 0x95, 0xf2, 0xb9, 0x0d, 0xa1, 0x6f,
-	0x1c, 0x5f, 0x5f, 0x21, 0x4e, 0xf9, 0x04, 0xc0, 0xe5, 0xa2, 0xde, 0x63, 0xce, 0xd3, 0x24, 0xd2,
-	0x2b, 0x34, 0xe2, 0x8f, 0x9a, 0x1d, 0xfe, 0xcf, 0x38, 0xfd, 0xed, 0x8b, 0x44, 0xed, 0x22, 0xba,
-	0x7a, 0x11, 0x1d, 0xb4, 0x56, 0xb9, 0x08, 0xc2, 0x79, 0x69, 0x09, 0x3b, 0xcf, 0x7e, 0x4c, 0x3c,
-	0x70, 0x3a, 0xf1, 0xc0, 0xef, 0x89, 0x07, 0x3e, 0x4e, 0xbd, 0xd6, 0xe9, 0xd4, 0x6b, 0xfd, 0x9a,
-	0x7a, 0xad, 0x57, 0x41, 0x9c, 0xa8, 0x51, 0x1e, 0x06, 0x11, 0x1b, 0xdb, 0x72, 0x7e, 0x4a, 0x42,
-	0xe9, 0x4a, 0xbf, 0x39, 0x2b, 0xae, 0x8e, 0x39, 0x95, 0xe1, 0x92, 0xbe, 0x10, 0xfb, 0x7f, 0x03,
-	0x00, 0x00, 0xff, 0xff, 0x9b, 0x3f, 0xc6, 0xb3, 0x05, 0x07, 0x00, 0x00,
+	// 697 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x95, 0x3f, 0x4f, 0x14, 0x4f,
+	0x18, 0xc7, 0x99, 0x5f, 0xf2, 0xc3, 0x64, 0x12, 0x48, 0x1c, 0x49, 0x0c, 0x17, 0x73, 0x46, 0x2d,
+	0x10, 0x8e, 0xdb, 0xe1, 0x38, 0x82, 0x40, 0x81, 0xe1, 0xa0, 0x10, 0x63, 0x81, 0x04, 0x1b, 0x9b,
+	0xcb, 0xec, 0x32, 0x2c, 0x1b, 0xf6, 0x76, 0x86, 0x9d, 0x39, 0x23, 0xb9, 0x5c, 0x63, 0x6d, 0x61,
+	0x62, 0xe3, 0x0b, 0xb0, 0xb1, 0xb3, 0xb7, 0x31, 0xb1, 0xb1, 0x24, 0x31, 0x31, 0x16, 0x16, 0x06,
+	0x7c, 0x21, 0x66, 0xe7, 0xcf, 0xe1, 0x82, 0xfb, 0xe7, 0xae, 0xbb, 0x99, 0xfd, 0x7e, 0x9f, 0xe7,
+	0xf3, 0x3c, 0x33, 0xcf, 0x1c, 0xbc, 0xb7, 0x4f, 0x45, 0x87, 0x09, 0xcc, 0x63, 0x76, 0x10, 0x84,
+	0x54, 0xe0, 0x17, 0x0d, 0x97, 0x4a, 0xd2, 0xc0, 0xc7, 0x5d, 0x1a, 0x9f, 0x38, 0x3c, 0x66, 0x92,
+	0xa1, 0x9b, 0x5a, 0xe4, 0x58, 0x91, 0x63, 0x44, 0x95, 0x29, 0x9f, 0xf9, 0x4c, 0x69, 0x70, 0xf2,
+	0x4b, 0xcb, 0x2b, 0xb7, 0x7c, 0xc6, 0xfc, 0x90, 0x62, 0xc2, 0x03, 0x4c, 0xa2, 0x88, 0x49, 0x22,
+	0x03, 0x16, 0x09, 0xf3, 0x75, 0xda, 0x7c, 0x55, 0x2b, 0xb7, 0x7b, 0x80, 0x49, 0x64, 0xf2, 0x54,
+	0x6a, 0xb9, 0x30, 0x6d, 0xb3, 0x6d, 0xc4, 0x8d, 0x7c, 0x71, 0x4c, 0x43, 0x9d, 0xf6, 0x30, 0xe0,
+	0xa2, 0x9c, 0x65, 0x5f, 0x12, 0xbf, 0x1d, 0xd3, 0xe3, 0x2e, 0x15, 0xd2, 0x5a, 0xe6, 0x0a, 0x90,
+	0x48, 0x4c, 0x3a, 0x56, 0x8b, 0xf3, 0xb5, 0xde, 0x21, 0x09, 0xa2, 0x76, 0x18, 0x44, 0x47, 0xd6,
+	0x50, 0xcf, 0x37, 0x10, 0xce, 0x53, 0xf2, 0x69, 0x8f, 0x25, 0xf2, 0xb6, 0x6e, 0xb8, 0x5e, 0x58,
+	0x4c, 0xbd, 0xc2, 0x2e, 0x11, 0x54, 0xbb, 0x07, 0xb1, 0x38, 0xf1, 0x83, 0x48, 0xb5, 0x42, 0x6b,
+	0x17, 0x3f, 0x4d, 0xc2, 0xff, 0x9f, 0x26, 0x12, 0xf4, 0x0e, 0xc0, 0x6b, 0x3b, 0x3a, 0x37, 0x9a,
+	0x77, 0x32, 0x0e, 0xd9, 0x51, 0x5a, 0x23, 0xdb, 0xd5, 0xdd, 0xa9, 0xd4, 0x4b, 0xaa, 0x05, 0x67,
+	0x91, 0xa0, 0x77, 0x17, 0x5e, 0x7d, 0xfb, 0xfd, 0xf6, 0xbf, 0x39, 0x74, 0x3f, 0xb3, 0x45, 0x83,
+	0x8d, 0x5e, 0x57, 0xd0, 0xb8, 0x8f, 0x3e, 0x00, 0x38, 0xb1, 0xb5, 0x47, 0xfc, 0xbd, 0x98, 0x44,
+	0xe2, 0x80, 0xc6, 0x02, 0x2d, 0xe6, 0xa7, 0x4c, 0x89, 0x2d, 0x66, 0x73, 0x28, 0x8f, 0x81, 0xc5,
+	0x0a, 0x76, 0x16, 0xcd, 0x64, 0xc2, 0xaa, 0x8b, 0x22, 0x07, 0x64, 0xaf, 0x01, 0x1c, 0xdf, 0x51,
+	0x17, 0x01, 0xd5, 0x0a, 0xfa, 0xa2, 0x54, 0x96, 0x6e, 0xbe, 0x9c, 0xd8, 0x60, 0xcd, 0x28, 0xac,
+	0x3b, 0xe8, 0x76, 0x76, 0x0f, 0x35, 0xc3, 0x67, 0x00, 0xaf, 0x3f, 0x13, 0x34, 0xde, 0xfd, 0x7b,
+	0x02, 0xd0, 0x72, 0x7e, 0xb2, 0x2b, 0x06, 0x0b, 0xf9, 0x60, 0x68, 0x9f, 0xe1, 0x5d, 0x53, 0xbc,
+	0x4b, 0x68, 0xd1, 0xf2, 0xa6, 0x26, 0x72, 0x00, 0x9d, 0xde, 0x35, 0xa7, 0xff, 0x1e, 0x40, 0x98,
+	0x44, 0x6e, 0x85, 0xcc, 0x3b, 0x12, 0x08, 0x17, 0x33, 0x68, 0xa5, 0x85, 0x5e, 0x28, 0x6f, 0x30,
+	0xb4, 0x4d, 0x45, 0x5b, 0x47, 0xb5, 0x7c, 0x5a, 0x57, 0xb9, 0x2c, 0xe6, 0x47, 0x00, 0x27, 0x93,
+	0x58, 0x9b, 0xc9, 0x64, 0x3f, 0x49, 0x26, 0x15, 0x35, 0x8b, 0x33, 0x5f, 0xa8, 0x2d, 0xee, 0xd2,
+	0x70, 0xa6, 0x2c, 0xe4, 0x2b, 0x17, 0x42, 0xbd, 0x38, 0x75, 0xf5, 0x84, 0x58, 0xe4, 0x2f, 0x00,
+	0x4e, 0xa4, 0xe2, 0x15, 0xcd, 0x55, 0x4a, 0x5c, 0x72, 0xae, 0x2e, 0x79, 0x0c, 0xef, 0xb6, 0xe2,
+	0xdd, 0x44, 0x1b, 0x43, 0xf0, 0xe2, 0x9e, 0x7e, 0x35, 0x23, 0xd2, 0xa1, 0x7d, 0xdc, 0x93, 0x24,
+	0xf6, 0xa9, 0xec, 0x27, 0x57, 0x7c, 0x2a, 0x49, 0xb2, 0xc1, 0x79, 0x18, 0x78, 0xea, 0x9c, 0x74,
+	0xfb, 0x57, 0x8b, 0xc1, 0x2e, 0x7b, 0x6c, 0x4d, 0x6b, 0xa3, 0x58, 0x4d, 0x69, 0x0d, 0x55, 0x5a,
+	0x0d, 0xcd, 0x66, 0x96, 0x46, 0x38, 0x4f, 0x1f, 0xc4, 0x77, 0x00, 0x6f, 0xfc, 0x23, 0x26, 0x5a,
+	0x19, 0x1a, 0xc3, 0x16, 0xb0, 0x3a, 0x82, 0xd3, 0xf0, 0x3f, 0x56, 0xfc, 0x5b, 0xa8, 0x55, 0x9a,
+	0x1f, 0xf7, 0xc8, 0x45, 0xac, 0xbe, 0xde, 0x54, 0x87, 0x84, 0x7e, 0x02, 0x38, 0x7d, 0x29, 0x4f,
+	0xeb, 0x64, 0x33, 0x0c, 0x68, 0x24, 0xb7, 0xb7, 0xd0, 0x7a, 0x3e, 0x64, 0xa6, 0xd1, 0x16, 0xf9,
+	0x70, 0x64, 0xbf, 0x29, 0x75, 0x5d, 0x95, 0xba, 0x82, 0x96, 0x4b, 0x94, 0xea, 0x29, 0xb3, 0xc0,
+	0x3d, 0xfd, 0xa3, 0x1d, 0xec, 0xf7, 0x5b, 0x8f, 0xbe, 0x9e, 0x55, 0xc1, 0xe9, 0x59, 0x15, 0xfc,
+	0x3a, 0xab, 0x82, 0x37, 0xe7, 0xd5, 0xb1, 0xd3, 0xf3, 0xea, 0xd8, 0x8f, 0xf3, 0xea, 0xd8, 0x73,
+	0xc7, 0x0f, 0xe4, 0x61, 0xd7, 0x75, 0x3c, 0xd6, 0x31, 0xb1, 0xeb, 0x21, 0x71, 0x85, 0xcd, 0xf3,
+	0xf2, 0x22, 0x93, 0x3c, 0xe1, 0x54, 0xb8, 0xe3, 0xea, 0xef, 0xb8, 0xf9, 0x27, 0x00, 0x00, 0xff,
+	0xff, 0xc5, 0x20, 0x1c, 0x8a, 0x83, 0x09, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -102,8 +110,17 @@ type QueryClient interface {
 	UserBlocks(ctx context.Context, in *QueryUserBlocksRequest, opts ...grpc.CallOption) (*QueryUserBlocksResponse, error)
 	// UserChainLinks queries chain links for the given user
 	UserChainLinks(ctx context.Context, in *QueryUserChainLinksRequest, opts ...grpc.CallOption) (*QueryUserChainLinksResponse, error)
+	// UserChainLink queries the chain link for the given user, chain name and
+	// target address
+	UserChainLink(ctx context.Context, in *QueryUserChainLinkRequest, opts ...grpc.CallOption) (*QueryUserChainLinkResponse, error)
 	// UserApplicationLinks queries application links for the given user
 	UserApplicationLinks(ctx context.Context, in *QueryUserApplicationLinksRequest, opts ...grpc.CallOption) (*QueryUserApplicationLinksResponse, error)
+	// UserApplicationLinks queries a single application link for a given user,
+	// searching via the application name and username
+	UserApplicationLink(ctx context.Context, in *QueryUserApplicationLinkRequest, opts ...grpc.CallOption) (*QueryUserApplicationLinkResponse, error)
+	// ApplicationLinkByClientID queries a single application link for a given
+	// client id.
+	ApplicationLinkByClientID(ctx context.Context, in *QueryApplicationLinkByClientIDRequest, opts ...grpc.CallOption) (*QueryApplicationLinkByClientIDResponse, error)
 }
 
 type queryClient struct {
@@ -168,6 +185,15 @@ func (c *queryClient) UserChainLinks(ctx context.Context, in *QueryUserChainLink
 	return out, nil
 }
 
+func (c *queryClient) UserChainLink(ctx context.Context, in *QueryUserChainLinkRequest, opts ...grpc.CallOption) (*QueryUserChainLinkResponse, error) {
+	out := new(QueryUserChainLinkResponse)
+	err := c.cc.Invoke(ctx, "/desmos.profiles.v1beta1.Query/UserChainLink", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *queryClient) UserApplicationLinks(ctx context.Context, in *QueryUserApplicationLinksRequest, opts ...grpc.CallOption) (*QueryUserApplicationLinksResponse, error) {
 	out := new(QueryUserApplicationLinksResponse)
 	err := c.cc.Invoke(ctx, "/desmos.profiles.v1beta1.Query/UserApplicationLinks", in, out, opts...)
@@ -177,6 +203,24 @@ func (c *queryClient) UserApplicationLinks(ctx context.Context, in *QueryUserApp
 	return out, nil
 }
 
+func (c *queryClient) UserApplicationLink(ctx context.Context, in *QueryUserApplicationLinkRequest, opts ...grpc.CallOption) (*QueryUserApplicationLinkResponse, error) {
+	out := new(QueryUserApplicationLinkResponse)
+	err := c.cc.Invoke(ctx, "/desmos.profiles.v1beta1.Query/UserApplicationLink", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *queryClient) ApplicationLinkByClientID(ctx context.Context, in *QueryApplicationLinkByClientIDRequest, opts ...grpc.CallOption) (*QueryApplicationLinkByClientIDResponse, error) {
+	out := new(QueryApplicationLinkByClientIDResponse)
+	err := c.cc.Invoke(ctx, "/desmos.profiles.v1beta1.Query/ApplicationLinkByClientID", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 // QueryServer is the server API for Query service.
 type QueryServer interface {
 	// Profile queries the profile of a specific user given their DTag or address.
@@ -195,8 +239,17 @@ type QueryServer interface {
 	UserBlocks(context.Context, *QueryUserBlocksRequest) (*QueryUserBlocksResponse, error)
 	// UserChainLinks queries chain links for the given user
 	UserChainLinks(context.Context, *QueryUserChainLinksRequest) (*QueryUserChainLinksResponse, error)
+	// UserChainLink queries the chain link for the given user, chain name and
+	// target address
+	UserChainLink(context.Context, *QueryUserChainLinkRequest) (*QueryUserChainLinkResponse, error)
 	// UserApplicationLinks queries application links for the given user
 	UserApplicationLinks(context.Context, *QueryUserApplicationLinksRequest) (*QueryUserApplicationLinksResponse, error)
+	// UserApplicationLinks queries a single application link for a given user,
+	// searching via the application name and username
+	UserApplicationLink(context.Context, *QueryUserApplicationLinkRequest) (*QueryUserApplicationLinkResponse, error)
+	// ApplicationLinkByClientID queries a single application link for a given
+	// client id.
+	ApplicationLinkByClientID(context.Context, *QueryApplicationLinkByClientIDRequest) (*QueryApplicationLinkByClientIDResponse, error)
 }
 
 // UnimplementedQueryServer can be embedded to have forward compatible implementations.
@@ -221,9 +274,18 @@ func (*UnimplementedQueryServer) UserBlocks(ctx context.Context, req *QueryUserB
 func (*UnimplementedQueryServer) UserChainLinks(ctx context.Context, req *QueryUserChainLinksRequest) (*QueryUserChainLinksResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method UserChainLinks not implemented")
 }
+func (*UnimplementedQueryServer) UserChainLink(ctx context.Context, req *QueryUserChainLinkRequest) (*QueryUserChainLinkResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method UserChainLink not implemented")
+}
 func (*UnimplementedQueryServer) UserApplicationLinks(ctx context.Context, req *QueryUserApplicationLinksRequest) (*QueryUserApplicationLinksResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method UserApplicationLinks not implemented")
 }
+func (*UnimplementedQueryServer) UserApplicationLink(ctx context.Context, req *QueryUserApplicationLinkRequest) (*QueryUserApplicationLinkResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method UserApplicationLink not implemented")
+}
+func (*UnimplementedQueryServer) ApplicationLinkByClientID(ctx context.Context, req *QueryApplicationLinkByClientIDRequest) (*QueryApplicationLinkByClientIDResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ApplicationLinkByClientID not implemented")
+}
 
 func RegisterQueryServer(s grpc1.Server, srv QueryServer) {
 	s.RegisterService(&_Query_serviceDesc, srv)
@@ -337,6 +399,24 @@ func _Query_UserChainLinks_Handler(srv interface{}, ctx context.Context, dec fun
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Query_UserChainLink_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(QueryUserChainLinkRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(QueryServer).UserChainLink(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/desmos.profiles.v1beta1.Query/UserChainLink",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(QueryServer).UserChainLink(ctx, req.(*QueryUserChainLinkRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _Query_UserApplicationLinks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(QueryUserApplicationLinksRequest)
 	if err := dec(in); err != nil {
@@ -355,6 +435,42 @@ func _Query_UserApplicationLinks_Handler(srv interface{}, ctx context.Context, d
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Query_UserApplicationLink_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(QueryUserApplicationLinkRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(QueryServer).UserApplicationLink(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/desmos.profiles.v1beta1.Query/UserApplicationLink",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(QueryServer).UserApplicationLink(ctx, req.(*QueryUserApplicationLinkRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Query_ApplicationLinkByClientID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(QueryApplicationLinkByClientIDRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(QueryServer).ApplicationLinkByClientID(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/desmos.profiles.v1beta1.Query/ApplicationLinkByClientID",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(QueryServer).ApplicationLinkByClientID(ctx, req.(*QueryApplicationLinkByClientIDRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 var _Query_serviceDesc = grpc.ServiceDesc{
 	ServiceName: "desmos.profiles.v1beta1.Query",
 	HandlerType: (*QueryServer)(nil),
@@ -383,10 +499,22 @@ var _Query_serviceDesc = grpc.ServiceDesc{
 			MethodName: "UserChainLinks",
 			Handler:    _Query_UserChainLinks_Handler,
 		},
+		{
+			MethodName: "UserChainLink",
+			Handler:    _Query_UserChainLink_Handler,
+		},
 		{
 			MethodName: "UserApplicationLinks",
 			Handler:    _Query_UserApplicationLinks_Handler,
 		},
+		{
+			MethodName: "UserApplicationLink",
+			Handler:    _Query_UserApplicationLink_Handler,
+		},
+		{
+			MethodName: "ApplicationLinkByClientID",
+			Handler:    _Query_ApplicationLinkByClientID_Handler,
+		},
 	},
 	Streams:  []grpc.StreamDesc{},
 	Metadata: "desmos/profiles/v1beta1/query.proto",
diff --git a/x/profiles/types/query.pb.gw.go b/x/profiles/types/query.pb.gw.go
index 7b160c03..74d46187 100644
--- a/x/profiles/types/query.pb.gw.go
+++ b/x/profiles/types/query.pb.gw.go
@@ -337,6 +337,104 @@ func local_request_Query_UserChainLinks_0(ctx context.Context, marshaler runtime
 
 }
 
+func request_Query_UserChainLink_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryUserChainLinkRequest
+	var metadata runtime.ServerMetadata
+
+	var (
+		val string
+		ok  bool
+		err error
+		_   = err
+	)
+
+	val, ok = pathParams["user"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "user")
+	}
+
+	protoReq.User, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "user", err)
+	}
+
+	val, ok = pathParams["chain_name"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "chain_name")
+	}
+
+	protoReq.ChainName, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "chain_name", err)
+	}
+
+	val, ok = pathParams["target"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "target")
+	}
+
+	protoReq.Target, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "target", err)
+	}
+
+	msg, err := client.UserChainLink(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Query_UserChainLink_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryUserChainLinkRequest
+	var metadata runtime.ServerMetadata
+
+	var (
+		val string
+		ok  bool
+		err error
+		_   = err
+	)
+
+	val, ok = pathParams["user"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "user")
+	}
+
+	protoReq.User, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "user", err)
+	}
+
+	val, ok = pathParams["chain_name"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "chain_name")
+	}
+
+	protoReq.ChainName, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "chain_name", err)
+	}
+
+	val, ok = pathParams["target"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "target")
+	}
+
+	protoReq.Target, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "target", err)
+	}
+
+	msg, err := server.UserChainLink(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
 var (
 	filter_Query_UserApplicationLinks_0 = &utilities.DoubleArray{Encoding: map[string]int{"user": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
 )
@@ -409,6 +507,158 @@ func local_request_Query_UserApplicationLinks_0(ctx context.Context, marshaler r
 
 }
 
+func request_Query_UserApplicationLink_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryUserApplicationLinkRequest
+	var metadata runtime.ServerMetadata
+
+	var (
+		val string
+		ok  bool
+		err error
+		_   = err
+	)
+
+	val, ok = pathParams["user"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "user")
+	}
+
+	protoReq.User, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "user", err)
+	}
+
+	val, ok = pathParams["application"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application")
+	}
+
+	protoReq.Application, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application", err)
+	}
+
+	val, ok = pathParams["username"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "username")
+	}
+
+	protoReq.Username, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "username", err)
+	}
+
+	msg, err := client.UserApplicationLink(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Query_UserApplicationLink_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryUserApplicationLinkRequest
+	var metadata runtime.ServerMetadata
+
+	var (
+		val string
+		ok  bool
+		err error
+		_   = err
+	)
+
+	val, ok = pathParams["user"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "user")
+	}
+
+	protoReq.User, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "user", err)
+	}
+
+	val, ok = pathParams["application"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application")
+	}
+
+	protoReq.Application, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application", err)
+	}
+
+	val, ok = pathParams["username"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "username")
+	}
+
+	protoReq.Username, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "username", err)
+	}
+
+	msg, err := server.UserApplicationLink(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
+func request_Query_ApplicationLinkByClientID_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryApplicationLinkByClientIDRequest
+	var metadata runtime.ServerMetadata
+
+	var (
+		val string
+		ok  bool
+		err error
+		_   = err
+	)
+
+	val, ok = pathParams["client_id"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id")
+	}
+
+	protoReq.ClientId, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err)
+	}
+
+	msg, err := client.ApplicationLinkByClientID(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Query_ApplicationLinkByClientID_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryApplicationLinkByClientIDRequest
+	var metadata runtime.ServerMetadata
+
+	var (
+		val string
+		ok  bool
+		err error
+		_   = err
+	)
+
+	val, ok = pathParams["client_id"]
+	if !ok {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "client_id")
+	}
+
+	protoReq.ClientId, err = runtime.String(val)
+
+	if err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "client_id", err)
+	}
+
+	msg, err := server.ApplicationLinkByClientID(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
 // RegisterQueryHandlerServer registers the http handlers for service Query to "mux".
 // UnaryRPC     :call QueryServer directly.
 // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@@ -535,6 +785,26 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv
 
 	})
 
+	mux.Handle("GET", pattern_Query_UserChainLink_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_Query_UserChainLink_0(rctx, inboundMarshaler, server, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Query_UserChainLink_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	mux.Handle("GET", pattern_Query_UserApplicationLinks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
@@ -555,6 +825,46 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv
 
 	})
 
+	mux.Handle("GET", pattern_Query_UserApplicationLink_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_Query_UserApplicationLink_0(rctx, inboundMarshaler, server, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Query_UserApplicationLink_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("GET", pattern_Query_ApplicationLinkByClientID_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_Query_ApplicationLinkByClientID_0(rctx, inboundMarshaler, server, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Query_ApplicationLinkByClientID_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	return nil
 }
 
@@ -716,6 +1026,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie
 
 	})
 
+	mux.Handle("GET", pattern_Query_UserChainLink_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_Query_UserChainLink_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Query_UserChainLink_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	mux.Handle("GET", pattern_Query_UserApplicationLinks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
@@ -736,6 +1066,46 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie
 
 	})
 
+	mux.Handle("GET", pattern_Query_UserApplicationLink_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_Query_UserApplicationLink_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Query_UserApplicationLink_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("GET", pattern_Query_ApplicationLinkByClientID_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_Query_ApplicationLinkByClientID_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Query_ApplicationLinkByClientID_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	return nil
 }
 
@@ -752,7 +1122,13 @@ var (
 
 	pattern_Query_UserChainLinks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"desmos", "profiles", "v1beta1", "chain-links", "user"}, "", runtime.AssumeColonVerbOpt(true)))
 
+	pattern_Query_UserChainLink_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5, 1, 0, 4, 1, 5, 6}, []string{"desmos", "profiles", "v1beta1", "chain-links", "user", "chain_name", "target"}, "", runtime.AssumeColonVerbOpt(true)))
+
 	pattern_Query_UserApplicationLinks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"desmos", "profiles", "v1beta1", "app-links", "user"}, "", runtime.AssumeColonVerbOpt(true)))
+
+	pattern_Query_UserApplicationLink_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5, 1, 0, 4, 1, 5, 6}, []string{"desmos", "profiles", "v1beta1", "app-links", "user", "application", "username"}, "", runtime.AssumeColonVerbOpt(true)))
+
+	pattern_Query_ApplicationLinkByClientID_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"desmos", "profiles", "v1beta1", "app-links", "clients", "client_id"}, "", runtime.AssumeColonVerbOpt(true)))
 )
 
 var (
@@ -768,5 +1144,11 @@ var (
 
 	forward_Query_UserChainLinks_0 = runtime.ForwardResponseMessage
 
+	forward_Query_UserChainLink_0 = runtime.ForwardResponseMessage
+
 	forward_Query_UserApplicationLinks_0 = runtime.ForwardResponseMessage
+
+	forward_Query_UserApplicationLink_0 = runtime.ForwardResponseMessage
+
+	forward_Query_ApplicationLinkByClientID_0 = runtime.ForwardResponseMessage
 )
diff --git a/x/profiles/types/query_app_links.go b/x/profiles/types/query_app_links.go
index afaab0ff..a8bca994 100644
--- a/x/profiles/types/query_app_links.go
+++ b/x/profiles/types/query_app_links.go
@@ -11,3 +11,19 @@ func NewQueryUserApplicationLinksRequest(user string, pageReq *query.PageRequest
 		Pagination: pageReq,
 	}
 }
+
+// NewQueryUserApplicationLinkRequest returns a new QueryUserApplicationLinkRequest instance
+func NewQueryUserApplicationLinkRequest(user, application, username string) *QueryUserApplicationLinkRequest {
+	return &QueryUserApplicationLinkRequest{
+		User:        user,
+		Application: application,
+		Username:    username,
+	}
+}
+
+// NewQueryApplicationLinkByClientIDRequest returns a new QueryApplicationLinkByClientIDRequest instance
+func NewQueryApplicationLinkByClientIDRequest(clientID string) *QueryApplicationLinkByClientIDRequest {
+	return &QueryApplicationLinkByClientIDRequest{
+		ClientId: clientID,
+	}
+}
diff --git a/x/profiles/types/query_app_links.pb.go b/x/profiles/types/query_app_links.pb.go
index 8331d1c2..97387ebb 100644
--- a/x/profiles/types/query_app_links.pb.go
+++ b/x/profiles/types/query_app_links.pb.go
@@ -27,8 +27,122 @@ var _ = math.Inf
 // proto package needs to be updated.
 const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
 
-// QueryUserApplicationLinksRequest represent the request used when querying the
-// application links of a specific user
+// QueryUserApplicationLinkRequest represents the request used when querying an
+// application link using an application name and username for a given user
+type QueryUserApplicationLinkRequest struct {
+	// User contains the Desmos profile address associated for which the link
+	// should be searched for
+	User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+	// Application represents the application name associated with the link
+	Application string `protobuf:"bytes,2,opt,name=application,proto3" json:"application,omitempty"`
+	// Username represents the username inside the application associated with the
+	// link
+	Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
+}
+
+func (m *QueryUserApplicationLinkRequest) Reset()         { *m = QueryUserApplicationLinkRequest{} }
+func (m *QueryUserApplicationLinkRequest) String() string { return proto.CompactTextString(m) }
+func (*QueryUserApplicationLinkRequest) ProtoMessage()    {}
+func (*QueryUserApplicationLinkRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_18d2a2b45fd238cb, []int{0}
+}
+func (m *QueryUserApplicationLinkRequest) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryUserApplicationLinkRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryUserApplicationLinkRequest.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *QueryUserApplicationLinkRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryUserApplicationLinkRequest.Merge(m, src)
+}
+func (m *QueryUserApplicationLinkRequest) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryUserApplicationLinkRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryUserApplicationLinkRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryUserApplicationLinkRequest proto.InternalMessageInfo
+
+func (m *QueryUserApplicationLinkRequest) GetUser() string {
+	if m != nil {
+		return m.User
+	}
+	return ""
+}
+
+func (m *QueryUserApplicationLinkRequest) GetApplication() string {
+	if m != nil {
+		return m.Application
+	}
+	return ""
+}
+
+func (m *QueryUserApplicationLinkRequest) GetUsername() string {
+	if m != nil {
+		return m.Username
+	}
+	return ""
+}
+
+// QueryUserApplicationLinkResponse represents the response to the query
+// allowing to get an application link for a specific user, searching via the
+// application name and username
+type QueryUserApplicationLinkResponse struct {
+	Link ApplicationLink `protobuf:"bytes,1,opt,name=link,proto3" json:"link"`
+}
+
+func (m *QueryUserApplicationLinkResponse) Reset()         { *m = QueryUserApplicationLinkResponse{} }
+func (m *QueryUserApplicationLinkResponse) String() string { return proto.CompactTextString(m) }
+func (*QueryUserApplicationLinkResponse) ProtoMessage()    {}
+func (*QueryUserApplicationLinkResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_18d2a2b45fd238cb, []int{1}
+}
+func (m *QueryUserApplicationLinkResponse) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryUserApplicationLinkResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryUserApplicationLinkResponse.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *QueryUserApplicationLinkResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryUserApplicationLinkResponse.Merge(m, src)
+}
+func (m *QueryUserApplicationLinkResponse) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryUserApplicationLinkResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryUserApplicationLinkResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryUserApplicationLinkResponse proto.InternalMessageInfo
+
+func (m *QueryUserApplicationLinkResponse) GetLink() ApplicationLink {
+	if m != nil {
+		return m.Link
+	}
+	return ApplicationLink{}
+}
+
+// QueryUserApplicationLinksRequest represents the request used when querying
+// the application links of a specific user
 type QueryUserApplicationLinksRequest struct {
 	User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
 	// Pagination defines an optional pagination for the request
@@ -39,7 +153,7 @@ func (m *QueryUserApplicationLinksRequest) Reset()         { *m = QueryUserAppli
 func (m *QueryUserApplicationLinksRequest) String() string { return proto.CompactTextString(m) }
 func (*QueryUserApplicationLinksRequest) ProtoMessage()    {}
 func (*QueryUserApplicationLinksRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_18d2a2b45fd238cb, []int{0}
+	return fileDescriptor_18d2a2b45fd238cb, []int{2}
 }
 func (m *QueryUserApplicationLinksRequest) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -94,7 +208,7 @@ func (m *QueryUserApplicationLinksResponse) Reset()         { *m = QueryUserAppl
 func (m *QueryUserApplicationLinksResponse) String() string { return proto.CompactTextString(m) }
 func (*QueryUserApplicationLinksResponse) ProtoMessage()    {}
 func (*QueryUserApplicationLinksResponse) Descriptor() ([]byte, []int) {
-	return fileDescriptor_18d2a2b45fd238cb, []int{1}
+	return fileDescriptor_18d2a2b45fd238cb, []int{3}
 }
 func (m *QueryUserApplicationLinksResponse) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -137,9 +251,108 @@ func (m *QueryUserApplicationLinksResponse) GetPagination() *query.PageResponse
 	return nil
 }
 
+// QueryApplicationLinkByClientIDRequest contains the data of the request that
+// can be used to get an application link based on a client id
+type QueryApplicationLinkByClientIDRequest struct {
+	// ClientID represents the ID of the client to which search the link for
+	ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
+}
+
+func (m *QueryApplicationLinkByClientIDRequest) Reset()         { *m = QueryApplicationLinkByClientIDRequest{} }
+func (m *QueryApplicationLinkByClientIDRequest) String() string { return proto.CompactTextString(m) }
+func (*QueryApplicationLinkByClientIDRequest) ProtoMessage()    {}
+func (*QueryApplicationLinkByClientIDRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_18d2a2b45fd238cb, []int{4}
+}
+func (m *QueryApplicationLinkByClientIDRequest) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryApplicationLinkByClientIDRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryApplicationLinkByClientIDRequest.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *QueryApplicationLinkByClientIDRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryApplicationLinkByClientIDRequest.Merge(m, src)
+}
+func (m *QueryApplicationLinkByClientIDRequest) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryApplicationLinkByClientIDRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryApplicationLinkByClientIDRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryApplicationLinkByClientIDRequest proto.InternalMessageInfo
+
+func (m *QueryApplicationLinkByClientIDRequest) GetClientId() string {
+	if m != nil {
+		return m.ClientId
+	}
+	return ""
+}
+
+// QueryApplicationLinkByClientIDResponse contains the data returned by the
+// request allowing to get an application link using a client id
+type QueryApplicationLinkByClientIDResponse struct {
+	Link ApplicationLink `protobuf:"bytes,1,opt,name=link,proto3" json:"link"`
+}
+
+func (m *QueryApplicationLinkByClientIDResponse) Reset() {
+	*m = QueryApplicationLinkByClientIDResponse{}
+}
+func (m *QueryApplicationLinkByClientIDResponse) String() string { return proto.CompactTextString(m) }
+func (*QueryApplicationLinkByClientIDResponse) ProtoMessage()    {}
+func (*QueryApplicationLinkByClientIDResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_18d2a2b45fd238cb, []int{5}
+}
+func (m *QueryApplicationLinkByClientIDResponse) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryApplicationLinkByClientIDResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryApplicationLinkByClientIDResponse.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *QueryApplicationLinkByClientIDResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryApplicationLinkByClientIDResponse.Merge(m, src)
+}
+func (m *QueryApplicationLinkByClientIDResponse) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryApplicationLinkByClientIDResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryApplicationLinkByClientIDResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryApplicationLinkByClientIDResponse proto.InternalMessageInfo
+
+func (m *QueryApplicationLinkByClientIDResponse) GetLink() ApplicationLink {
+	if m != nil {
+		return m.Link
+	}
+	return ApplicationLink{}
+}
+
 func init() {
+	proto.RegisterType((*QueryUserApplicationLinkRequest)(nil), "desmos.profiles.v1beta1.QueryUserApplicationLinkRequest")
+	proto.RegisterType((*QueryUserApplicationLinkResponse)(nil), "desmos.profiles.v1beta1.QueryUserApplicationLinkResponse")
 	proto.RegisterType((*QueryUserApplicationLinksRequest)(nil), "desmos.profiles.v1beta1.QueryUserApplicationLinksRequest")
 	proto.RegisterType((*QueryUserApplicationLinksResponse)(nil), "desmos.profiles.v1beta1.QueryUserApplicationLinksResponse")
+	proto.RegisterType((*QueryApplicationLinkByClientIDRequest)(nil), "desmos.profiles.v1beta1.QueryApplicationLinkByClientIDRequest")
+	proto.RegisterType((*QueryApplicationLinkByClientIDResponse)(nil), "desmos.profiles.v1beta1.QueryApplicationLinkByClientIDResponse")
 }
 
 func init() {
@@ -147,30 +360,113 @@ func init() {
 }
 
 var fileDescriptor_18d2a2b45fd238cb = []byte{
-	// 368 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x91, 0xc1, 0x4e, 0xfa, 0x40,
-	0x10, 0xc6, 0xbb, 0xff, 0x3f, 0x9a, 0x58, 0x6e, 0x8d, 0x89, 0xd0, 0x98, 0x5a, 0x39, 0x68, 0x63,
-	0xc2, 0x6e, 0xc0, 0x27, 0x90, 0x18, 0xf5, 0xe0, 0x41, 0x9b, 0x78, 0xf1, 0x42, 0xb6, 0xb0, 0xd4,
-	0xc6, 0xd2, 0x59, 0xba, 0x5b, 0x23, 0x17, 0x9f, 0xc1, 0x67, 0xf1, 0x29, 0x38, 0x72, 0xf4, 0x64,
-	0x0c, 0xbc, 0x88, 0xe9, 0xee, 0x22, 0x84, 0x88, 0xde, 0x66, 0xf2, 0x7d, 0xfb, 0xcd, 0x6f, 0x76,
-	0xec, 0x66, 0x9f, 0x89, 0x21, 0x08, 0xc2, 0x73, 0x18, 0x24, 0x29, 0x13, 0xe4, 0xa9, 0x15, 0x31,
-	0x49, 0x5b, 0x64, 0x54, 0xb0, 0x7c, 0xdc, 0xa5, 0x9c, 0x77, 0xd3, 0x24, 0x7b, 0x14, 0x98, 0xe7,
-	0x20, 0xc1, 0xd9, 0xd3, 0x76, 0xbc, 0xb0, 0x63, 0x63, 0x77, 0x77, 0x63, 0x88, 0x41, 0x79, 0x48,
-	0x59, 0x69, 0xbb, 0xbb, 0x1f, 0x03, 0xc4, 0x29, 0x23, 0x94, 0x27, 0x84, 0x66, 0x19, 0x48, 0x2a,
-	0x13, 0xc8, 0x4c, 0x98, 0x5b, 0x37, 0xaa, 0xea, 0xa2, 0x62, 0x40, 0x68, 0x36, 0x36, 0x12, 0xde,
-	0x84, 0x35, 0x84, 0x3e, 0x4b, 0xc5, 0x3a, 0x97, 0x5b, 0xef, 0x41, 0xe9, 0xef, 0x6a, 0x02, 0xdd,
-	0x18, 0xe9, 0x44, 0x77, 0x24, 0xa2, 0x82, 0xe9, 0xad, 0xbe, 0xc3, 0x38, 0x8d, 0x93, 0x4c, 0x21,
-	0x69, 0x6f, 0xe3, 0xc5, 0xf6, 0x6f, 0x4b, 0xc7, 0x9d, 0x60, 0xf9, 0x19, 0xe7, 0x69, 0xd2, 0x53,
-	0xea, 0x75, 0x39, 0x29, 0x64, 0xa3, 0x82, 0x09, 0xe9, 0x38, 0x76, 0xa5, 0x10, 0x2c, 0xaf, 0x21,
-	0x1f, 0x05, 0x3b, 0xa1, 0xaa, 0x9d, 0x0b, 0xdb, 0x5e, 0x66, 0xd5, 0xfe, 0xf9, 0x28, 0xa8, 0xb6,
-	0x8f, 0xb0, 0xc1, 0x28, 0x07, 0x63, 0x35, 0x78, 0xf1, 0x5b, 0xf8, 0x86, 0xc6, 0xcc, 0xe4, 0x85,
-	0x2b, 0x2f, 0x1b, 0x6f, 0xc8, 0x3e, 0xfc, 0x05, 0x40, 0x70, 0xc8, 0x04, 0x73, 0xce, 0xed, 0x2d,
-	0xb5, 0x7b, 0x0d, 0xf9, 0xff, 0x83, 0x6a, 0x3b, 0xc0, 0x1b, 0x8e, 0x82, 0xd7, 0x12, 0x3a, 0x95,
-	0xc9, 0xc7, 0x81, 0x15, 0xea, 0xc7, 0xce, 0xe5, 0x0f, 0xcc, 0xc7, 0x7f, 0x32, 0x6b, 0x84, 0x55,
-	0xe8, 0xce, 0xd5, 0x64, 0xe6, 0xa1, 0xe9, 0xcc, 0x43, 0x9f, 0x33, 0x0f, 0xbd, 0xce, 0x3d, 0x6b,
-	0x3a, 0xf7, 0xac, 0xf7, 0xb9, 0x67, 0xdd, 0xe3, 0x38, 0x91, 0x0f, 0x45, 0x84, 0x7b, 0x30, 0x24,
-	0x9a, 0xb1, 0x99, 0xd2, 0x48, 0x98, 0x9a, 0x3c, 0x2f, 0xcf, 0x2b, 0xc7, 0x9c, 0x89, 0x68, 0x5b,
-	0x5d, 0xe1, 0xf4, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x04, 0x99, 0x2a, 0xde, 0x95, 0x02, 0x00, 0x00,
+	// 459 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0xd1, 0x6e, 0xd3, 0x30,
+	0x14, 0x6d, 0x58, 0x41, 0x9b, 0xfb, 0x16, 0x21, 0xd1, 0x05, 0x94, 0x95, 0x48, 0x8c, 0x09, 0x69,
+	0xb6, 0x36, 0xbe, 0x80, 0x52, 0x01, 0x93, 0x78, 0x80, 0x4a, 0xbc, 0xf0, 0x52, 0x39, 0xc9, 0x6d,
+	0xb0, 0xe6, 0xd8, 0x5e, 0xec, 0x20, 0xf2, 0xc2, 0x37, 0xf0, 0x2d, 0x7c, 0xc5, 0x1e, 0xf7, 0xc8,
+	0x13, 0x42, 0xed, 0x8f, 0xa0, 0xd8, 0x5e, 0x3b, 0x2a, 0xb2, 0x4a, 0x88, 0x37, 0x5f, 0x9f, 0x73,
+	0xef, 0x3d, 0xe7, 0x58, 0x46, 0xc7, 0x39, 0xe8, 0x52, 0x6a, 0xa2, 0x2a, 0x39, 0x67, 0x1c, 0x34,
+	0xf9, 0x7c, 0x92, 0x82, 0xa1, 0x27, 0xe4, 0xa2, 0x86, 0xaa, 0x99, 0x51, 0xa5, 0x66, 0x9c, 0x89,
+	0x73, 0x8d, 0x55, 0x25, 0x8d, 0x0c, 0x1f, 0x38, 0x3a, 0xbe, 0xa6, 0x63, 0x4f, 0x8f, 0xee, 0x17,
+	0xb2, 0x90, 0x96, 0x43, 0xda, 0x93, 0xa3, 0x47, 0x8f, 0x0a, 0x29, 0x0b, 0x0e, 0x84, 0x2a, 0x46,
+	0xa8, 0x10, 0xd2, 0x50, 0xc3, 0xa4, 0xf0, 0xc3, 0xa2, 0x7d, 0x8f, 0xda, 0x2a, 0xad, 0xe7, 0x84,
+	0x8a, 0xc6, 0x43, 0xb8, 0x4b, 0x56, 0x29, 0x73, 0xe0, 0x7a, 0x53, 0x57, 0xb4, 0x9f, 0xc9, 0x96,
+	0x3f, 0x73, 0x0a, 0x5c, 0xe1, 0xa1, 0x67, 0xae, 0x22, 0x29, 0xd5, 0xe0, 0x5c, 0xad, 0x86, 0x29,
+	0x5a, 0x30, 0x61, 0x25, 0x39, 0x6e, 0xa2, 0xd1, 0xc1, 0xfb, 0x96, 0xf1, 0x41, 0x43, 0xf5, 0x42,
+	0x29, 0xce, 0x32, 0x8b, 0xbe, 0x65, 0xe2, 0x7c, 0x0a, 0x17, 0x35, 0x68, 0x13, 0x86, 0xa8, 0x5f,
+	0x6b, 0xa8, 0x86, 0xc1, 0x28, 0x38, 0xda, 0x9b, 0xda, 0x73, 0x38, 0x42, 0x03, 0xba, 0x66, 0x0f,
+	0xef, 0x58, 0xe8, 0xe6, 0x55, 0x18, 0xa1, 0xdd, 0x96, 0x29, 0x68, 0x09, 0xc3, 0x1d, 0x0b, 0xaf,
+	0xea, 0x64, 0x8e, 0x46, 0xdd, 0x4b, 0xb5, 0x92, 0x42, 0x43, 0x38, 0x46, 0xfd, 0xd6, 0xae, 0xdd,
+	0x3a, 0x38, 0x3d, 0xc2, 0x1d, 0xcf, 0x80, 0x37, 0xfa, 0xc7, 0xfd, 0xcb, 0x9f, 0x07, 0xbd, 0xa9,
+	0xed, 0x4d, 0xbe, 0x76, 0xef, 0xd1, 0xb7, 0xb9, 0x7b, 0x85, 0xd0, 0x3a, 0x28, 0x6b, 0x6e, 0x70,
+	0x7a, 0x88, 0x7d, 0xc6, 0x6d, 0xaa, 0xd8, 0xa6, 0xba, 0xd2, 0xf0, 0x8e, 0x16, 0xe0, 0xe7, 0x4d,
+	0x6f, 0x74, 0x26, 0xdf, 0x03, 0xf4, 0xf8, 0x16, 0x01, 0xde, 0xe9, 0x04, 0xdd, 0xb5, 0x0f, 0x3b,
+	0x0c, 0x46, 0x3b, 0xff, 0x60, 0xd5, 0x35, 0x87, 0xaf, 0xff, 0xa2, 0xf9, 0xe9, 0x56, 0xcd, 0x4e,
+	0xc2, 0x1f, 0xa2, 0x27, 0xe8, 0x89, 0xd5, 0xbc, 0xb9, 0xad, 0x79, 0xc9, 0x19, 0x08, 0x73, 0x36,
+	0xb9, 0x4e, 0xee, 0x21, 0xda, 0xcb, 0xec, 0xd5, 0x8c, 0xe5, 0x3e, 0xbe, 0x5d, 0x77, 0x71, 0x96,
+	0x27, 0x1c, 0x1d, 0x6e, 0x9b, 0xf2, 0xff, 0x1e, 0x7a, 0xfc, 0xe6, 0x72, 0x11, 0x07, 0x57, 0x8b,
+	0x38, 0xf8, 0xb5, 0x88, 0x83, 0x6f, 0xcb, 0xb8, 0x77, 0xb5, 0x8c, 0x7b, 0x3f, 0x96, 0x71, 0xef,
+	0x23, 0x2e, 0x98, 0xf9, 0x54, 0xa7, 0x38, 0x93, 0x25, 0x71, 0x93, 0x8f, 0x39, 0x4d, 0xb5, 0x3f,
+	0x93, 0x2f, 0xeb, 0xff, 0x66, 0x1a, 0x05, 0x3a, 0xbd, 0x67, 0xbf, 0xc5, 0xf3, 0xdf, 0x01, 0x00,
+	0x00, 0xff, 0xff, 0xe8, 0x31, 0x86, 0x83, 0x26, 0x04, 0x00, 0x00,
+}
+
+func (m *QueryUserApplicationLinkRequest) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *QueryUserApplicationLinkRequest) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryUserApplicationLinkRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.Username) > 0 {
+		i -= len(m.Username)
+		copy(dAtA[i:], m.Username)
+		i = encodeVarintQueryAppLinks(dAtA, i, uint64(len(m.Username)))
+		i--
+		dAtA[i] = 0x1a
+	}
+	if len(m.Application) > 0 {
+		i -= len(m.Application)
+		copy(dAtA[i:], m.Application)
+		i = encodeVarintQueryAppLinks(dAtA, i, uint64(len(m.Application)))
+		i--
+		dAtA[i] = 0x12
+	}
+	if len(m.User) > 0 {
+		i -= len(m.User)
+		copy(dAtA[i:], m.User)
+		i = encodeVarintQueryAppLinks(dAtA, i, uint64(len(m.User)))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *QueryUserApplicationLinkResponse) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *QueryUserApplicationLinkResponse) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryUserApplicationLinkResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	{
+		size, err := m.Link.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintQueryAppLinks(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
 }
 
 func (m *QueryUserApplicationLinksRequest) Marshal() (dAtA []byte, err error) {
@@ -264,6 +560,69 @@ func (m *QueryUserApplicationLinksResponse) MarshalToSizedBuffer(dAtA []byte) (i
 	return len(dAtA) - i, nil
 }
 
+func (m *QueryApplicationLinkByClientIDRequest) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *QueryApplicationLinkByClientIDRequest) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryApplicationLinkByClientIDRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.ClientId) > 0 {
+		i -= len(m.ClientId)
+		copy(dAtA[i:], m.ClientId)
+		i = encodeVarintQueryAppLinks(dAtA, i, uint64(len(m.ClientId)))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *QueryApplicationLinkByClientIDResponse) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *QueryApplicationLinkByClientIDResponse) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryApplicationLinkByClientIDResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	{
+		size, err := m.Link.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintQueryAppLinks(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
 func encodeVarintQueryAppLinks(dAtA []byte, offset int, v uint64) int {
 	offset -= sovQueryAppLinks(v)
 	base := offset
@@ -275,6 +634,38 @@ func encodeVarintQueryAppLinks(dAtA []byte, offset int, v uint64) int {
 	dAtA[offset] = uint8(v)
 	return base
 }
+func (m *QueryUserApplicationLinkRequest) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = len(m.User)
+	if l > 0 {
+		n += 1 + l + sovQueryAppLinks(uint64(l))
+	}
+	l = len(m.Application)
+	if l > 0 {
+		n += 1 + l + sovQueryAppLinks(uint64(l))
+	}
+	l = len(m.Username)
+	if l > 0 {
+		n += 1 + l + sovQueryAppLinks(uint64(l))
+	}
+	return n
+}
+
+func (m *QueryUserApplicationLinkResponse) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = m.Link.Size()
+	n += 1 + l + sovQueryAppLinks(uint64(l))
+	return n
+}
+
 func (m *QueryUserApplicationLinksRequest) Size() (n int) {
 	if m == nil {
 		return 0
@@ -311,16 +702,269 @@ func (m *QueryUserApplicationLinksResponse) Size() (n int) {
 	return n
 }
 
-func sovQueryAppLinks(x uint64) (n int) {
-	return (math_bits.Len64(x|1) + 6) / 7
-}
-func sozQueryAppLinks(x uint64) (n int) {
-	return sovQueryAppLinks(uint64((x << 1) ^ uint64((int64(x) >> 63))))
-}
-func (m *QueryUserApplicationLinksRequest) Unmarshal(dAtA []byte) error {
-	l := len(dAtA)
-	iNdEx := 0
-	for iNdEx < l {
+func (m *QueryApplicationLinkByClientIDRequest) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = len(m.ClientId)
+	if l > 0 {
+		n += 1 + l + sovQueryAppLinks(uint64(l))
+	}
+	return n
+}
+
+func (m *QueryApplicationLinkByClientIDResponse) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = m.Link.Size()
+	n += 1 + l + sovQueryAppLinks(uint64(l))
+	return n
+}
+
+func sovQueryAppLinks(x uint64) (n int) {
+	return (math_bits.Len64(x|1) + 6) / 7
+}
+func sozQueryAppLinks(x uint64) (n int) {
+	return sovQueryAppLinks(uint64((x << 1) ^ uint64((int64(x) >> 63))))
+}
+func (m *QueryUserApplicationLinkRequest) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowQueryAppLinks
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: QueryUserApplicationLinkRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryUserApplicationLinkRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field User", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryAppLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.User = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Application", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryAppLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Application = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Username", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryAppLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Username = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipQueryAppLinks(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *QueryUserApplicationLinkResponse) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowQueryAppLinks
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: QueryUserApplicationLinkResponse: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryUserApplicationLinkResponse: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Link", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryAppLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := m.Link.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipQueryAppLinks(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *QueryUserApplicationLinksRequest) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
 		preIndex := iNdEx
 		var wire uint64
 		for shift := uint(0); ; shift += 7 {
@@ -555,6 +1199,171 @@ func (m *QueryUserApplicationLinksResponse) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *QueryApplicationLinkByClientIDRequest) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowQueryAppLinks
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: QueryApplicationLinkByClientIDRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryApplicationLinkByClientIDRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field ClientId", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryAppLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.ClientId = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipQueryAppLinks(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *QueryApplicationLinkByClientIDResponse) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowQueryAppLinks
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: QueryApplicationLinkByClientIDResponse: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryApplicationLinkByClientIDResponse: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Link", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryAppLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := m.Link.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipQueryAppLinks(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthQueryAppLinks
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func skipQueryAppLinks(dAtA []byte) (n int, err error) {
 	l := len(dAtA)
 	iNdEx := 0
diff --git a/x/profiles/types/query_chain_links.pb.go b/x/profiles/types/query_chain_links.pb.go
index 98515741..c6bcef8d 100644
--- a/x/profiles/types/query_chain_links.pb.go
+++ b/x/profiles/types/query_chain_links.pb.go
@@ -27,6 +27,118 @@ var _ = math.Inf
 // proto package needs to be updated.
 const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
 
+// QueryUserChainLinkRequest represents the request that should be used in order
+// to retrieve the link associated with the provided user, for the given chain
+// and having the given target address
+type QueryUserChainLinkRequest struct {
+	// User represents the Desmos address of the user to which search the link for
+	User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+	// ChainName contains the name of the chain to which search the link for
+	ChainName string `protobuf:"bytes,2,opt,name=chain_name,json=chainName,proto3" json:"chain_name,omitempty"`
+	// Target must contain the external address to which query the link for
+	Target string `protobuf:"bytes,3,opt,name=target,proto3" json:"target,omitempty"`
+}
+
+func (m *QueryUserChainLinkRequest) Reset()         { *m = QueryUserChainLinkRequest{} }
+func (m *QueryUserChainLinkRequest) String() string { return proto.CompactTextString(m) }
+func (*QueryUserChainLinkRequest) ProtoMessage()    {}
+func (*QueryUserChainLinkRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_c44c4be38a628772, []int{0}
+}
+func (m *QueryUserChainLinkRequest) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryUserChainLinkRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryUserChainLinkRequest.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *QueryUserChainLinkRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryUserChainLinkRequest.Merge(m, src)
+}
+func (m *QueryUserChainLinkRequest) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryUserChainLinkRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryUserChainLinkRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryUserChainLinkRequest proto.InternalMessageInfo
+
+func (m *QueryUserChainLinkRequest) GetUser() string {
+	if m != nil {
+		return m.User
+	}
+	return ""
+}
+
+func (m *QueryUserChainLinkRequest) GetChainName() string {
+	if m != nil {
+		return m.ChainName
+	}
+	return ""
+}
+
+func (m *QueryUserChainLinkRequest) GetTarget() string {
+	if m != nil {
+		return m.Target
+	}
+	return ""
+}
+
+// QueryUserChainLinkResponse contains the data that is returned when querying a
+// specific chain link
+type QueryUserChainLinkResponse struct {
+	Link ChainLink `protobuf:"bytes,1,opt,name=link,proto3" json:"link"`
+}
+
+func (m *QueryUserChainLinkResponse) Reset()         { *m = QueryUserChainLinkResponse{} }
+func (m *QueryUserChainLinkResponse) String() string { return proto.CompactTextString(m) }
+func (*QueryUserChainLinkResponse) ProtoMessage()    {}
+func (*QueryUserChainLinkResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_c44c4be38a628772, []int{1}
+}
+func (m *QueryUserChainLinkResponse) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryUserChainLinkResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryUserChainLinkResponse.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *QueryUserChainLinkResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryUserChainLinkResponse.Merge(m, src)
+}
+func (m *QueryUserChainLinkResponse) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryUserChainLinkResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryUserChainLinkResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryUserChainLinkResponse proto.InternalMessageInfo
+
+func (m *QueryUserChainLinkResponse) GetLink() ChainLink {
+	if m != nil {
+		return m.Link
+	}
+	return ChainLink{}
+}
+
 // QueryUserChainLinksRequest is the request type for the
 // Query/UserChainLinks RPC endpoint
 type QueryUserChainLinksRequest struct {
@@ -39,7 +151,7 @@ func (m *QueryUserChainLinksRequest) Reset()         { *m = QueryUserChainLinksR
 func (m *QueryUserChainLinksRequest) String() string { return proto.CompactTextString(m) }
 func (*QueryUserChainLinksRequest) ProtoMessage()    {}
 func (*QueryUserChainLinksRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_c44c4be38a628772, []int{0}
+	return fileDescriptor_c44c4be38a628772, []int{2}
 }
 func (m *QueryUserChainLinksRequest) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -94,7 +206,7 @@ func (m *QueryUserChainLinksResponse) Reset()         { *m = QueryUserChainLinks
 func (m *QueryUserChainLinksResponse) String() string { return proto.CompactTextString(m) }
 func (*QueryUserChainLinksResponse) ProtoMessage()    {}
 func (*QueryUserChainLinksResponse) Descriptor() ([]byte, []int) {
-	return fileDescriptor_c44c4be38a628772, []int{1}
+	return fileDescriptor_c44c4be38a628772, []int{3}
 }
 func (m *QueryUserChainLinksResponse) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -138,6 +250,8 @@ func (m *QueryUserChainLinksResponse) GetPagination() *query.PageResponse {
 }
 
 func init() {
+	proto.RegisterType((*QueryUserChainLinkRequest)(nil), "desmos.profiles.v1beta1.QueryUserChainLinkRequest")
+	proto.RegisterType((*QueryUserChainLinkResponse)(nil), "desmos.profiles.v1beta1.QueryUserChainLinkResponse")
 	proto.RegisterType((*QueryUserChainLinksRequest)(nil), "desmos.profiles.v1beta1.QueryUserChainLinksRequest")
 	proto.RegisterType((*QueryUserChainLinksResponse)(nil), "desmos.profiles.v1beta1.QueryUserChainLinksResponse")
 }
@@ -147,30 +261,111 @@ func init() {
 }
 
 var fileDescriptor_c44c4be38a628772 = []byte{
-	// 368 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0xcf, 0x4e, 0xea, 0x40,
-	0x14, 0xc6, 0x3b, 0xf7, 0x72, 0x6f, 0x62, 0xd9, 0x35, 0x26, 0x42, 0x35, 0x95, 0xb0, 0x50, 0x62,
-	0xe2, 0x8c, 0xe0, 0xde, 0x05, 0x26, 0xea, 0xc2, 0x85, 0x36, 0x71, 0xe3, 0x86, 0x4c, 0xe1, 0x50,
-	0x1a, 0xcb, 0x9c, 0xd2, 0x69, 0x0d, 0xbc, 0x85, 0x4f, 0xe1, 0xb3, 0xb0, 0x64, 0xe9, 0xca, 0x18,
-	0x78, 0x11, 0xd3, 0x99, 0xe1, 0x4f, 0x22, 0xc4, 0xdd, 0x9c, 0x9c, 0xf3, 0x7d, 0xdf, 0x6f, 0x66,
-	0x8e, 0xcd, 0x7a, 0x20, 0x87, 0x28, 0x59, 0x92, 0x62, 0x3f, 0x8a, 0x41, 0xb2, 0xd7, 0x66, 0x00,
-	0x19, 0x6f, 0xb2, 0x51, 0x0e, 0xe9, 0xa4, 0xd3, 0x1d, 0xf0, 0x48, 0x74, 0xe2, 0x48, 0xbc, 0x48,
-	0x9a, 0xa4, 0x98, 0xa1, 0x73, 0xa0, 0x05, 0x74, 0x29, 0xa0, 0x46, 0xe0, 0xee, 0x87, 0x18, 0xa2,
-	0x9a, 0x61, 0xc5, 0x49, 0x8f, 0xbb, 0x47, 0x21, 0x62, 0x18, 0x03, 0xe3, 0x49, 0xc4, 0xb8, 0x10,
-	0x98, 0xf1, 0x2c, 0x42, 0x61, 0xcc, 0xdc, 0xaa, 0xe9, 0xaa, 0x2a, 0xc8, 0xfb, 0x8c, 0x8b, 0x89,
-	0x69, 0x5d, 0xec, 0x02, 0x1b, 0x62, 0x0f, 0x62, 0xf9, 0x93, 0xcc, 0xad, 0x76, 0xb1, 0x50, 0x74,
-	0x34, 0x83, 0x2e, 0x4c, 0xeb, 0x4c, 0x57, 0x2c, 0xe0, 0x12, 0xf4, 0xcd, 0x56, 0x76, 0x09, 0x0f,
-	0x23, 0xa1, 0xa0, 0xf4, 0x6c, 0x7d, 0x6c, 0xbb, 0x8f, 0xc5, 0xc4, 0x93, 0x84, 0xf4, 0xba, 0x08,
-	0xb9, 0x2f, 0x32, 0x7c, 0x18, 0xe5, 0x20, 0x33, 0xc7, 0xb1, 0x4b, 0xb9, 0x84, 0xb4, 0x42, 0x6a,
-	0xa4, 0xb1, 0xe7, 0xab, 0xb3, 0x73, 0x63, 0xdb, 0x6b, 0x97, 0xca, 0x9f, 0x1a, 0x69, 0x94, 0x5b,
-	0x27, 0xd4, 0x00, 0x14, 0x91, 0x54, 0x45, 0x2e, 0x5f, 0x8a, 0x3e, 0xf0, 0x10, 0x8c, 0x9f, 0xbf,
-	0xa1, 0xac, 0xbf, 0x13, 0xfb, 0x70, 0x6b, 0xb4, 0x4c, 0x50, 0x48, 0x70, 0xae, 0xec, 0x7f, 0xea,
-	0xbe, 0x15, 0x52, 0xfb, 0xdb, 0x28, 0xb7, 0xea, 0x74, 0xc7, 0x57, 0xd0, 0x95, 0xb6, 0x5d, 0x9a,
-	0x7e, 0x1e, 0x5b, 0xbe, 0x96, 0x39, 0xb7, 0x5b, 0x38, 0x4f, 0x7f, 0xe5, 0xd4, 0xe1, 0x9b, 0xa0,
-	0xed, 0xbb, 0xe9, 0xdc, 0x23, 0xb3, 0xb9, 0x47, 0xbe, 0xe6, 0x1e, 0x79, 0x5b, 0x78, 0xd6, 0x6c,
-	0xe1, 0x59, 0x1f, 0x0b, 0xcf, 0x7a, 0xa6, 0x61, 0x94, 0x0d, 0xf2, 0x80, 0x76, 0x71, 0x68, 0x36,
-	0xeb, 0x3c, 0xe6, 0x81, 0x5c, 0x6e, 0xd9, 0x78, 0xfd, 0x9d, 0xd9, 0x24, 0x01, 0x19, 0xfc, 0x57,
-	0x6f, 0x7e, 0xf9, 0x1d, 0x00, 0x00, 0xff, 0xff, 0x0a, 0xd8, 0xa6, 0x8b, 0x87, 0x02, 0x00, 0x00,
+	// 420 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0xbd, 0xae, 0xd3, 0x30,
+	0x14, 0x4e, 0xb8, 0xe5, 0x4a, 0xd7, 0x77, 0x8b, 0x10, 0xa4, 0x01, 0xc2, 0x55, 0x06, 0x40, 0x48,
+	0xd8, 0xb4, 0xac, 0x88, 0xa1, 0x48, 0xc0, 0x80, 0x10, 0x44, 0x62, 0xe9, 0x52, 0x39, 0xed, 0xa9,
+	0x6b, 0x35, 0xb1, 0xd3, 0xd8, 0x41, 0xed, 0x5b, 0xf0, 0x14, 0x3c, 0x4b, 0xc7, 0x8e, 0x4c, 0x08,
+	0xb5, 0x2f, 0x82, 0x62, 0xbb, 0x3f, 0x12, 0x29, 0x88, 0xcd, 0x27, 0xe7, 0x7c, 0xe7, 0xfb, 0x89,
+	0x8d, 0xc8, 0x04, 0x54, 0x21, 0x15, 0x29, 0x2b, 0x39, 0xe5, 0x39, 0x28, 0xf2, 0xb5, 0x97, 0x81,
+	0xa6, 0x3d, 0xb2, 0xa8, 0xa1, 0x5a, 0x8d, 0xc6, 0x33, 0xca, 0xc5, 0x28, 0xe7, 0x62, 0xae, 0x70,
+	0x59, 0x49, 0x2d, 0x83, 0x7b, 0x16, 0x80, 0xf7, 0x00, 0xec, 0x00, 0xd1, 0x1d, 0x26, 0x99, 0x34,
+	0x33, 0xa4, 0x39, 0xd9, 0xf1, 0xe8, 0x01, 0x93, 0x92, 0xe5, 0x40, 0x68, 0xc9, 0x09, 0x15, 0x42,
+	0x6a, 0xaa, 0xb9, 0x14, 0x6e, 0x59, 0xd4, 0x75, 0x5d, 0x53, 0x65, 0xf5, 0x94, 0x50, 0xb1, 0x72,
+	0xad, 0x17, 0xe7, 0x84, 0x15, 0x72, 0x02, 0xb9, 0xfa, 0x53, 0x59, 0xd4, 0x1d, 0xcb, 0x06, 0x31,
+	0xb2, 0x1a, 0x6c, 0xe1, 0x5a, 0xcf, 0x6c, 0x45, 0x32, 0xaa, 0xc0, 0x3a, 0x3b, 0xac, 0x2b, 0x29,
+	0xe3, 0xc2, 0x88, 0xb2, 0xb3, 0xc9, 0x14, 0x75, 0x3f, 0x37, 0x13, 0x5f, 0x14, 0x54, 0x6f, 0x1a,
+	0x92, 0x0f, 0x5c, 0xcc, 0x53, 0x58, 0xd4, 0xa0, 0x74, 0x10, 0xa0, 0x4e, 0xad, 0xa0, 0x0a, 0xfd,
+	0x1b, 0xff, 0xe9, 0x55, 0x6a, 0xce, 0xc1, 0x43, 0x84, 0xac, 0x18, 0x41, 0x0b, 0x08, 0x6f, 0x99,
+	0xce, 0x95, 0xf9, 0xf2, 0x91, 0x16, 0x10, 0xdc, 0x45, 0x97, 0x9a, 0x56, 0x0c, 0x74, 0x78, 0x61,
+	0x5a, 0xae, 0x4a, 0x86, 0x28, 0x6a, 0xe3, 0x51, 0xa5, 0x14, 0x0a, 0x82, 0x57, 0xa8, 0xd3, 0x78,
+	0x33, 0x44, 0xd7, 0xfd, 0x04, 0x9f, 0x49, 0x1d, 0x1f, 0x90, 0x83, 0xce, 0xfa, 0xe7, 0x23, 0x2f,
+	0x35, 0xa8, 0x64, 0xd9, 0xb6, 0x5b, 0xfd, 0xcd, 0xc4, 0x5b, 0x84, 0x8e, 0x49, 0x18, 0x13, 0xd7,
+	0xfd, 0xc7, 0xd8, 0x85, 0xd8, 0xc4, 0x86, 0x4d, 0x6c, 0x07, 0xde, 0x4f, 0x94, 0x81, 0xdb, 0x97,
+	0x9e, 0x20, 0x93, 0xef, 0x3e, 0xba, 0xdf, 0x4a, 0xed, 0x7c, 0xbd, 0x46, 0xb7, 0xcd, 0x3f, 0x0b,
+	0xfd, 0x9b, 0x8b, 0xff, 0x32, 0x66, 0x61, 0xc1, 0xbb, 0x16, 0x9d, 0x4f, 0xfe, 0xa9, 0xd3, 0x92,
+	0x9f, 0x0a, 0x1d, 0xbc, 0x5f, 0x6f, 0x63, 0x7f, 0xb3, 0x8d, 0xfd, 0x5f, 0xdb, 0xd8, 0xff, 0xb6,
+	0x8b, 0xbd, 0xcd, 0x2e, 0xf6, 0x7e, 0xec, 0x62, 0x6f, 0x88, 0x19, 0xd7, 0xb3, 0x3a, 0xc3, 0x63,
+	0x59, 0xb8, 0xd7, 0xf1, 0x3c, 0xa7, 0x99, 0xda, 0xbf, 0x94, 0xe5, 0xf1, 0x4a, 0xea, 0x55, 0x09,
+	0x2a, 0xbb, 0x34, 0xf7, 0xe6, 0xe5, 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x63, 0x42, 0xc8, 0xc6,
+	0x4b, 0x03, 0x00, 0x00,
+}
+
+func (m *QueryUserChainLinkRequest) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *QueryUserChainLinkRequest) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryUserChainLinkRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.Target) > 0 {
+		i -= len(m.Target)
+		copy(dAtA[i:], m.Target)
+		i = encodeVarintQueryChainLinks(dAtA, i, uint64(len(m.Target)))
+		i--
+		dAtA[i] = 0x1a
+	}
+	if len(m.ChainName) > 0 {
+		i -= len(m.ChainName)
+		copy(dAtA[i:], m.ChainName)
+		i = encodeVarintQueryChainLinks(dAtA, i, uint64(len(m.ChainName)))
+		i--
+		dAtA[i] = 0x12
+	}
+	if len(m.User) > 0 {
+		i -= len(m.User)
+		copy(dAtA[i:], m.User)
+		i = encodeVarintQueryChainLinks(dAtA, i, uint64(len(m.User)))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *QueryUserChainLinkResponse) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *QueryUserChainLinkResponse) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryUserChainLinkResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	{
+		size, err := m.Link.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintQueryChainLinks(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
 }
 
 func (m *QueryUserChainLinksRequest) Marshal() (dAtA []byte, err error) {
@@ -275,6 +470,38 @@ func encodeVarintQueryChainLinks(dAtA []byte, offset int, v uint64) int {
 	dAtA[offset] = uint8(v)
 	return base
 }
+func (m *QueryUserChainLinkRequest) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = len(m.User)
+	if l > 0 {
+		n += 1 + l + sovQueryChainLinks(uint64(l))
+	}
+	l = len(m.ChainName)
+	if l > 0 {
+		n += 1 + l + sovQueryChainLinks(uint64(l))
+	}
+	l = len(m.Target)
+	if l > 0 {
+		n += 1 + l + sovQueryChainLinks(uint64(l))
+	}
+	return n
+}
+
+func (m *QueryUserChainLinkResponse) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = m.Link.Size()
+	n += 1 + l + sovQueryChainLinks(uint64(l))
+	return n
+}
+
 func (m *QueryUserChainLinksRequest) Size() (n int) {
 	if m == nil {
 		return 0
@@ -317,6 +544,235 @@ func sovQueryChainLinks(x uint64) (n int) {
 func sozQueryChainLinks(x uint64) (n int) {
 	return sovQueryChainLinks(uint64((x << 1) ^ uint64((int64(x) >> 63))))
 }
+func (m *QueryUserChainLinkRequest) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowQueryChainLinks
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: QueryUserChainLinkRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryUserChainLinkRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field User", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryChainLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.User = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field ChainName", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryChainLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.ChainName = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Target", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryChainLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Target = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipQueryChainLinks(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *QueryUserChainLinkResponse) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowQueryChainLinks
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: QueryUserChainLinkResponse: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryUserChainLinkResponse: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Link", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQueryChainLinks
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := m.Link.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipQueryChainLinks(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthQueryChainLinks
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func (m *QueryUserChainLinksRequest) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
diff --git a/x/staging/posts/types/query.pb.go b/x/staging/posts/types/query.pb.go
index 2556365a..c7fa144b 100644
--- a/x/staging/posts/types/query.pb.go
+++ b/x/staging/posts/types/query.pb.go
@@ -33,8 +33,8 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
 
 // QueryPostsRequest is the request type for the Query/Posts RPC method.
 type QueryPostsRequest struct {
-	// SubspaceId represents the ID of the subspace to which to query the posts for.
-	// Providing an empty or invalid subspace id will return an error.
+	// SubspaceId represents the ID of the subspace to which to query the posts
+	// for. Providing an empty or invalid subspace id will return an error.
 	SubspaceId string `protobuf:"bytes,1,opt,name=subspace_id,json=subspaceId,proto3" json:"subspace_id,omitempty"`
 	// pagination defines an optional pagination for the request.
 	Pagination *query.PageRequest `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"`


Test patch:
diff --git a/x/profiles/keeper/grpc_query_test.go b/x/profiles/keeper/grpc_query_test.go
index daffbf19..0303c2c5 100644
--- a/x/profiles/keeper/grpc_query_test.go
+++ b/x/profiles/keeper/grpc_query_test.go
@@ -282,6 +282,91 @@ func (suite *KeeperTestSuite) Test_UserChainLinks() {
 	}
 }
 
+func (suite *KeeperTestSuite) Test_UserChainLink() {
+	var pubKey, err = sdk.GetPubKeyFromBech32(
+		sdk.Bech32PubKeyTypeAccPub,
+		"cosmospub1addwnpepqvryxhhqhw52c4ny5twtfzf3fsrjqhx0x5cuya0fylw0wu0eqptykeqhr4d",
+	)
+	suite.Require().NoError(err)
+
+	usecases := []struct {
+		name      string
+		store     func(ctx sdk.Context)
+		req       *types.QueryUserChainLinkRequest
+		shouldErr bool
+		expRes    *types.QueryUserChainLinkResponse
+	}{
+		{
+			name: "not found link returns error",
+			req: &types.QueryUserChainLinkRequest{
+				User:      "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+				ChainName: "cosmos",
+				Target:    "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+			},
+			shouldErr: true,
+		},
+		{
+			name: "existing chain link returns proper response",
+			store: func(ctx sdk.Context) {
+				address := "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47"
+				suite.ak.SetAccount(ctx, suite.CreateProfileFromAddress(address))
+
+				link := types.NewChainLink(
+					address,
+					types.NewBech32Address("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns", "cosmos"),
+					types.NewProof(
+						pubKey,
+						"909e38994b1583d3f14384c2e9a03c90064e8fd8e19b780bb0ba303dfe671a27287da04d0ce096ce9a140bd070ee36818f5519eb2070a16971efd8143855524b",
+						"text",
+					),
+					types.NewChainConfig("cosmos"),
+					suite.testData.profile.CreationDate,
+				)
+				suite.Require().NoError(suite.k.SaveChainLink(ctx, link))
+			},
+			req: &types.QueryUserChainLinkRequest{
+				User:      "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+				ChainName: "cosmos",
+				Target:    "cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns",
+			},
+			shouldErr: false,
+			expRes: &types.QueryUserChainLinkResponse{
+				Link: types.NewChainLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					types.NewBech32Address("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns", "cosmos"),
+					types.NewProof(
+						pubKey,
+						"909e38994b1583d3f14384c2e9a03c90064e8fd8e19b780bb0ba303dfe671a27287da04d0ce096ce9a140bd070ee36818f5519eb2070a16971efd8143855524b",
+						"text",
+					),
+					types.NewChainConfig("cosmos"),
+					suite.testData.profile.CreationDate,
+				),
+			},
+		},
+	}
+
+	for _, uc := range usecases {
+		uc := uc
+		suite.Run(uc.name, func() {
+			ctx, _ := suite.ctx.CacheContext()
+			if uc.store != nil {
+				uc.store(ctx)
+			}
+
+			res, err := suite.k.UserChainLink(sdk.WrapSDKContext(ctx), uc.req)
+			if uc.shouldErr {
+				suite.Require().Error(err)
+			} else {
+				suite.Require().NoError(err)
+				suite.Require().NotNil(res)
+
+				suite.Require().Equal(uc.expRes, res)
+			}
+		})
+	}
+}
+
 func (suite *KeeperTestSuite) Test_UserRelationships() {
 	usecases := []struct {
 		name                string
@@ -458,3 +543,169 @@ func (suite *KeeperTestSuite) TestQueryServer_UserApplicationLinks() {
 		})
 	}
 }
+
+func (suite *KeeperTestSuite) TestQueryServer_UserApplicationLink() {
+	usecases := []struct {
+		name        string
+		store       func(ctx sdk.Context)
+		req         *types.QueryUserApplicationLinkRequest
+		shouldErr   bool
+		expResponse *types.QueryUserApplicationLinkResponse
+	}{
+		{
+			name:      "not found link returns error",
+			req:       types.NewQueryUserApplicationLinkRequest("user", "application", "username"),
+			shouldErr: true,
+		},
+		{
+			name: "valid request returns proper response",
+			store: func(ctx sdk.Context) {
+				profile := suite.CreateProfileFromAddress("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+						types.NewData("twitter", "twitteruser"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							-1,
+							1,
+							types.NewOracleRequestCallData(
+								"twitter",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+			},
+			req: types.NewQueryUserApplicationLinkRequest(
+				"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+				"twitter",
+				"twitteruser",
+			),
+			shouldErr: false,
+			expResponse: &types.QueryUserApplicationLinkResponse{
+				Link: types.NewApplicationLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					types.NewData("twitter", "twitteruser"),
+					types.ApplicationLinkStateInitialized,
+					types.NewOracleRequest(
+						-1,
+						1,
+						types.NewOracleRequestCallData(
+							"twitter",
+							"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+						),
+						"client_id",
+					),
+					nil,
+					time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+				),
+			},
+		},
+	}
+
+	for _, uc := range usecases {
+		uc := uc
+		suite.Run(uc.name, func() {
+			ctx, _ := suite.ctx.CacheContext()
+			if uc.store != nil {
+				uc.store(ctx)
+			}
+
+			res, err := suite.k.UserApplicationLink(sdk.WrapSDKContext(ctx), uc.req)
+			if uc.shouldErr {
+				suite.Require().Error(err)
+			} else {
+				suite.Require().NoError(err)
+				suite.Require().Equal(uc.expResponse, res)
+			}
+		})
+	}
+}
+
+func (suite *KeeperTestSuite) TestQueryServer_ApplicationLinkByClientID() {
+	usecases := []struct {
+		name        string
+		store       func(ctx sdk.Context)
+		req         *types.QueryApplicationLinkByClientIDRequest
+		shouldErr   bool
+		expResponse *types.QueryApplicationLinkByClientIDResponse
+	}{
+		{
+			name:      "not found link returns error",
+			req:       types.NewQueryApplicationLinkByClientIDRequest("client_id"),
+			shouldErr: true,
+		},
+		{
+			name: "valid request returns proper response",
+			store: func(ctx sdk.Context) {
+				profile := suite.CreateProfileFromAddress("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+						types.NewData("twitter", "twitteruser"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							-1,
+							1,
+							types.NewOracleRequestCallData(
+								"twitter",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+			},
+			req:       types.NewQueryApplicationLinkByClientIDRequest("client_id"),
+			shouldErr: false,
+			expResponse: &types.QueryApplicationLinkByClientIDResponse{
+				Link: types.NewApplicationLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					types.NewData("twitter", "twitteruser"),
+					types.ApplicationLinkStateInitialized,
+					types.NewOracleRequest(
+						-1,
+						1,
+						types.NewOracleRequestCallData(
+							"twitter",
+							"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+						),
+						"client_id",
+					),
+					nil,
+					time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+				),
+			},
+		},
+	}
+
+	for _, uc := range usecases {
+		uc := uc
+		suite.Run(uc.name, func() {
+			ctx, _ := suite.ctx.CacheContext()
+			if uc.store != nil {
+				uc.store(ctx)
+			}
+
+			res, err := suite.k.ApplicationLinkByClientID(sdk.WrapSDKContext(ctx), uc.req)
+			if uc.shouldErr {
+				suite.Require().Error(err)
+			} else {
+				suite.Require().NoError(err)
+				suite.Require().Equal(uc.expResponse, res)
+			}
+		})
+	}
+}