Repository: desmos-labs/desmos
Pull request #793 (2022-03-23 11:45:08)

feat: reverse search links
## Description
This PR adds the ability to reverse search for app links and chain links owners. The new following queries have been added: 
- search for app link owners using an application name (optional), and username (optional, works only if the application is specified as well)
- search for chain link owners using a chain name (optional), and target address (optional, works only if the chain name is specified as well)  

Closes: #777
Closes: #794

---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/desmos-labs/desmos/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://docs.cosmos.network/v0.44/building-modules/intro.html)
- [ ] included the necessary unit and integration [tests](https://github.com/desmos-labs/desmos/blob/master/CONTRIBUTING.md#testing)
- [x] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)


Patch:
diff --git a/.changeset/entries/ed739ae62d392b19a07694f596ae5facae8dbd18ca8eb70bd6be0bc775aee829.yaml b/.changeset/entries/ed739ae62d392b19a07694f596ae5facae8dbd18ca8eb70bd6be0bc775aee829.yaml
new file mode 100644
index 00000000..a53b700e
--- /dev/null
+++ b/.changeset/entries/ed739ae62d392b19a07694f596ae5facae8dbd18ca8eb70bd6be0bc775aee829.yaml
@@ -0,0 +1,6 @@
+type: feat
+module: x/profiles
+pull_request: 793
+description: Added the ability to reverse search application links and chain links
+backward_compatible: true
+date: 2022-03-23T14:27:24.394374729Z
diff --git a/client/docs/swagger-ui/swagger.yaml b/client/docs/swagger-ui/swagger.yaml
index 292ee004..e8833620 100644
--- a/client/docs/swagger-ui/swagger.yaml
+++ b/client/docs/swagger-ui/swagger.yaml
@@ -762,215 +762,726 @@ paths:
           type: string
       tags:
         - Query
-  /desmos/profiles/v2/chain-links:
+  /desmos/profiles/v2/app-links/owners:
     get:
       summary: |-
-        ChainLinks queries the chain links associated to the given user, if
-        provided. Otherwise it queries all the chain links stored.
-      operationId: ChainLinks
+        ApplicationLinkOwners queries for the owners of applications links,
+        optionally searching for a specific application and username.
+      operationId: ApplicationLinkOwners
       responses:
         '200':
           description: A successful response.
           schema:
             type: object
             properties:
-              links:
+              owners:
                 type: array
                 items:
                   type: object
                   properties:
                     user:
                       type: string
-                      title: User defines the destination profile address to link
-                    address:
-                      type: object
-                      properties:
-                        type_url:
-                          type: string
-                          description: >-
-                            A URL/resource name that uniquely identifies the
-                            type of the serialized
+                    application:
+                      type: string
+                    username:
+                      type: string
+                  title: >-
+                    ApplicationLinkOwnerDetails contains the details of a single
+                    application
 
-                            protocol buffer message. This string must contain at
-                            least
+                    link owner
+                title: Addresses of the application links owners
+              pagination:
+                title: Pagination defines the pagination response
+                type: object
+                properties:
+                  next_key:
+                    type: string
+                    format: byte
+                    title: |-
+                      next_key is the key to be passed to PageRequest.key to
+                      query the next page most efficiently
+                  total:
+                    type: string
+                    format: uint64
+                    title: >-
+                      total is total number of results available if
+                      PageRequest.count_total
 
-                            one "/" character. The last segment of the URL's
-                            path must represent
+                      was set, its value is undefined otherwise
+                description: >-
+                  PageResponse is to be embedded in gRPC response messages where
+                  the
 
-                            the fully qualified name of the type (as in
+                  corresponding request message has used PageRequest.
 
-                            `path/google.protobuf.Duration`). The name should be
-                            in a canonical form
+                   message SomeResponse {
+                           repeated Bar results = 1;
+                           PageResponse page = 2;
+                   }
+            description: >-
+              QueryApplicationLinkOwnersResponse contains the data returned by
+              the request
 
-                            (e.g., leading "." is not accepted).
+              allowing to get application link owners.
+        default:
+          description: An unexpected error response
+          schema:
+            type: object
+            properties:
+              error:
+                type: string
+              code:
+                type: integer
+                format: int32
+              message:
+                type: string
+              details:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    type_url:
+                      type: string
+                      description: >-
+                        A URL/resource name that uniquely identifies the type of
+                        the serialized
 
+                        protocol buffer message. This string must contain at
+                        least
 
-                            In practice, teams usually precompile into the
-                            binary all types that they
+                        one "/" character. The last segment of the URL's path
+                        must represent
 
-                            expect it to use in the context of Any. However, for
-                            URLs which use the
+                        the fully qualified name of the type (as in
 
-                            scheme `http`, `https`, or no scheme, one can
-                            optionally set up a type
+                        `path/google.protobuf.Duration`). The name should be in
+                        a canonical form
 
-                            server that maps type URLs to message definitions as
-                            follows:
+                        (e.g., leading "." is not accepted).
 
 
-                            * If no scheme is provided, `https` is assumed.
+                        In practice, teams usually precompile into the binary
+                        all types that they
 
-                            * An HTTP GET on the URL must yield a
-                            [google.protobuf.Type][]
-                              value in binary format, or produce an error.
-                            * Applications are allowed to cache lookup results
-                            based on the
-                              URL, or have them precompiled into a binary to avoid any
-                              lookup. Therefore, binary compatibility needs to be preserved
-                              on changes to types. (Use versioned type names to manage
-                              breaking changes.)
+                        expect it to use in the context of Any. However, for
+                        URLs which use the
 
-                            Note: this functionality is not currently available
-                            in the official
+                        scheme `http`, `https`, or no scheme, one can optionally
+                        set up a type
 
-                            protobuf release, and it is not used for type URLs
-                            beginning with
+                        server that maps type URLs to message definitions as
+                        follows:
 
-                            type.googleapis.com.
 
+                        * If no scheme is provided, `https` is assumed.
 
-                            Schemes other than `http`, `https` (or the empty
-                            scheme) might be
+                        * An HTTP GET on the URL must yield a
+                        [google.protobuf.Type][]
+                          value in binary format, or produce an error.
+                        * Applications are allowed to cache lookup results based
+                        on the
+                          URL, or have them precompiled into a binary to avoid any
+                          lookup. Therefore, binary compatibility needs to be preserved
+                          on changes to types. (Use versioned type names to manage
+                          breaking changes.)
 
-                            used with implementation specific semantics.
-                        value:
-                          type: string
-                          format: byte
-                          description: >-
-                            Must be a valid serialized protocol buffer of the
-                            above specified type.
-                      description: >-
-                        `Any` contains an arbitrary serialized protocol buffer
-                        message along with a
+                        Note: this functionality is not currently available in
+                        the official
 
-                        URL that describes the type of the serialized message.
+                        protobuf release, and it is not used for type URLs
+                        beginning with
 
+                        type.googleapis.com.
 
-                        Protobuf library provides support to pack/unpack Any
-                        values in the form
 
-                        of utility functions or additional generated methods of
-                        the Any type.
+                        Schemes other than `http`, `https` (or the empty scheme)
+                        might be
 
+                        used with implementation specific semantics.
+                    value:
+                      type: string
+                      format: byte
+                      description: >-
+                        Must be a valid serialized protocol buffer of the above
+                        specified type.
+                  description: >-
+                    `Any` contains an arbitrary serialized protocol buffer
+                    message along with a
 
-                        Example 1: Pack and unpack a message in C++.
+                    URL that describes the type of the serialized message.
 
-                            Foo foo = ...;
-                            Any any;
-                            any.PackFrom(foo);
-                            ...
-                            if (any.UnpackTo(&foo)) {
-                              ...
-                            }
 
-                        Example 2: Pack and unpack a message in Java.
+                    Protobuf library provides support to pack/unpack Any values
+                    in the form
 
-                            Foo foo = ...;
-                            Any any = Any.pack(foo);
-                            ...
-                            if (any.is(Foo.class)) {
-                              foo = any.unpack(Foo.class);
-                            }
+                    of utility functions or additional generated methods of the
+                    Any type.
 
-                         Example 3: Pack and unpack a message in Python.
 
-                            foo = Foo(...)
-                            any = Any()
-                            any.Pack(foo)
-                            ...
-                            if any.Is(Foo.DESCRIPTOR):
-                              any.Unpack(foo)
-                              ...
+                    Example 1: Pack and unpack a message in C++.
 
-                         Example 4: Pack and unpack a message in Go
+                        Foo foo = ...;
+                        Any any;
+                        any.PackFrom(foo);
+                        ...
+                        if (any.UnpackTo(&foo)) {
+                          ...
+                        }
 
-                             foo := &pb.Foo{...}
-                             any, err := ptypes.MarshalAny(foo)
-                             ...
-                             foo := &pb.Foo{}
-                             if err := ptypes.UnmarshalAny(any, foo); err != nil {
-                               ...
-                             }
+                    Example 2: Pack and unpack a message in Java.
 
-                        The pack methods provided by protobuf library will by
-                        default use
+                        Foo foo = ...;
+                        Any any = Any.pack(foo);
+                        ...
+                        if (any.is(Foo.class)) {
+                          foo = any.unpack(Foo.class);
+                        }
 
-                        'type.googleapis.com/full.type.name' as the type URL and
-                        the unpack
+                     Example 3: Pack and unpack a message in Python.
 
-                        methods only use the fully qualified type name after the
-                        last '/'
+                        foo = Foo(...)
+                        any = Any()
+                        any.Pack(foo)
+                        ...
+                        if any.Is(Foo.DESCRIPTOR):
+                          any.Unpack(foo)
+                          ...
 
-                        in the type URL, for example "foo.bar.com/x/y.z" will
-                        yield type
+                     Example 4: Pack and unpack a message in Go
 
-                        name "y.z".
+                         foo := &pb.Foo{...}
+                         any, err := ptypes.MarshalAny(foo)
+                         ...
+                         foo := &pb.Foo{}
+                         if err := ptypes.UnmarshalAny(any, foo); err != nil {
+                           ...
+                         }
 
+                    The pack methods provided by protobuf library will by
+                    default use
 
+                    'type.googleapis.com/full.type.name' as the type URL and the
+                    unpack
 
-                        JSON
+                    methods only use the fully qualified type name after the
+                    last '/'
 
-                        ====
+                    in the type URL, for example "foo.bar.com/x/y.z" will yield
+                    type
 
-                        The JSON representation of an `Any` value uses the
-                        regular
+                    name "y.z".
 
-                        representation of the deserialized, embedded message,
-                        with an
 
-                        additional field `@type` which contains the type URL.
-                        Example:
 
-                            package google.profile;
-                            message Person {
-                              string first_name = 1;
-                              string last_name = 2;
-                            }
+                    JSON
+
+                    ====
+
+                    The JSON representation of an `Any` value uses the regular
+
+                    representation of the deserialized, embedded message, with
+                    an
+
+                    additional field `@type` which contains the type URL.
+                    Example:
+
+                        package google.profile;
+                        message Person {
+                          string first_name = 1;
+                          string last_name = 2;
+                        }
+
+                        {
+                          "@type": "type.googleapis.com/google.profile.Person",
+                          "firstName": <string>,
+                          "lastName": <string>
+                        }
+
+                    If the embedded message type is well-known and has a custom
+                    JSON
+
+                    representation, that representation will be embedded adding
+                    a field
+
+                    `value` which holds the custom JSON in addition to the
+                    `@type`
+
+                    field. Example (for message [google.protobuf.Duration][]):
+
+                        {
+                          "@type": "type.googleapis.com/google.protobuf.Duration",
+                          "value": "1.212s"
+                        }
+      parameters:
+        - name: application
+          description: >-
+            (Optional) Application name to search link owners of. If not
+            specified, all
+
+            links stored will be searched instead.
+          in: query
+          required: false
+          type: string
+        - name: username
+          description: |-
+            (Optional) Username to search for. This will only be used if the
+            application is specified as well.
+          in: query
+          required: false
+          type: string
+        - name: pagination.key
+          description: |-
+            key is a value returned in PageResponse.next_key to begin
+            querying the next page most efficiently. Only one of offset or key
+            should be set.
+          in: query
+          required: false
+          type: string
+          format: byte
+        - name: pagination.offset
+          description: >-
+            offset is a numeric offset that can be used when key is unavailable.
+
+            It is less efficient than using key. Only one of offset or key
+            should
+
+            be set.
+          in: query
+          required: false
+          type: string
+          format: uint64
+        - name: pagination.limit
+          description: >-
+            limit is the total number of results to be returned in the result
+            page.
+
+            If left empty it will default to a value to be set by each app.
+          in: query
+          required: false
+          type: string
+          format: uint64
+        - name: pagination.count_total
+          description: >-
+            count_total is set to true  to indicate that the result set should
+            include
+
+            a count of the total number of items available for pagination in
+            UIs.
+
+            count_total is only respected when offset is used. It is ignored
+            when key
+
+            is set.
+          in: query
+          required: false
+          type: boolean
+          format: boolean
+        - name: pagination.reverse
+          description: >-
+            reverse is set to true if results are to be returned in the
+            descending order.
+
+
+            Since: cosmos-sdk 0.43
+          in: query
+          required: false
+          type: boolean
+          format: boolean
+      tags:
+        - Query
+  /desmos/profiles/v2/chain-links:
+    get:
+      summary: |-
+        ChainLinks queries the chain links associated to the given user, if
+        provided. Otherwise it queries all the chain links stored.
+      operationId: ChainLinks
+      responses:
+        '200':
+          description: A successful response.
+          schema:
+            type: object
+            properties:
+              links:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    user:
+                      type: string
+                      title: User defines the destination profile address to link
+                    address:
+                      type: object
+                      properties:
+                        type_url:
+                          type: string
+                          description: >-
+                            A URL/resource name that uniquely identifies the
+                            type of the serialized
+
+                            protocol buffer message. This string must contain at
+                            least
+
+                            one "/" character. The last segment of the URL's
+                            path must represent
+
+                            the fully qualified name of the type (as in
+
+                            `path/google.protobuf.Duration`). The name should be
+                            in a canonical form
+
+                            (e.g., leading "." is not accepted).
+
+
+                            In practice, teams usually precompile into the
+                            binary all types that they
+
+                            expect it to use in the context of Any. However, for
+                            URLs which use the
+
+                            scheme `http`, `https`, or no scheme, one can
+                            optionally set up a type
+
+                            server that maps type URLs to message definitions as
+                            follows:
+
+
+                            * If no scheme is provided, `https` is assumed.
+
+                            * An HTTP GET on the URL must yield a
+                            [google.protobuf.Type][]
+                              value in binary format, or produce an error.
+                            * Applications are allowed to cache lookup results
+                            based on the
+                              URL, or have them precompiled into a binary to avoid any
+                              lookup. Therefore, binary compatibility needs to be preserved
+                              on changes to types. (Use versioned type names to manage
+                              breaking changes.)
+
+                            Note: this functionality is not currently available
+                            in the official
+
+                            protobuf release, and it is not used for type URLs
+                            beginning with
+
+                            type.googleapis.com.
+
+
+                            Schemes other than `http`, `https` (or the empty
+                            scheme) might be
+
+                            used with implementation specific semantics.
+                        value:
+                          type: string
+                          format: byte
+                          description: >-
+                            Must be a valid serialized protocol buffer of the
+                            above specified type.
+                      description: >-
+                        `Any` contains an arbitrary serialized protocol buffer
+                        message along with a
+
+                        URL that describes the type of the serialized message.
+
+
+                        Protobuf library provides support to pack/unpack Any
+                        values in the form
+
+                        of utility functions or additional generated methods of
+                        the Any type.
+
+
+                        Example 1: Pack and unpack a message in C++.
+
+                            Foo foo = ...;
+                            Any any;
+                            any.PackFrom(foo);
+                            ...
+                            if (any.UnpackTo(&foo)) {
+                              ...
+                            }
+
+                        Example 2: Pack and unpack a message in Java.
+
+                            Foo foo = ...;
+                            Any any = Any.pack(foo);
+                            ...
+                            if (any.is(Foo.class)) {
+                              foo = any.unpack(Foo.class);
+                            }
+
+                         Example 3: Pack and unpack a message in Python.
+
+                            foo = Foo(...)
+                            any = Any()
+                            any.Pack(foo)
+                            ...
+                            if any.Is(Foo.DESCRIPTOR):
+                              any.Unpack(foo)
+                              ...
+
+                         Example 4: Pack and unpack a message in Go
+
+                             foo := &pb.Foo{...}
+                             any, err := ptypes.MarshalAny(foo)
+                             ...
+                             foo := &pb.Foo{}
+                             if err := ptypes.UnmarshalAny(any, foo); err != nil {
+                               ...
+                             }
+
+                        The pack methods provided by protobuf library will by
+                        default use
+
+                        'type.googleapis.com/full.type.name' as the type URL and
+                        the unpack
+
+                        methods only use the fully qualified type name after the
+                        last '/'
+
+                        in the type URL, for example "foo.bar.com/x/y.z" will
+                        yield type
+
+                        name "y.z".
+
+
+
+                        JSON
+
+                        ====
+
+                        The JSON representation of an `Any` value uses the
+                        regular
+
+                        representation of the deserialized, embedded message,
+                        with an
+
+                        additional field `@type` which contains the type URL.
+                        Example:
+
+                            package google.profile;
+                            message Person {
+                              string first_name = 1;
+                              string last_name = 2;
+                            }
+
+                            {
+                              "@type": "type.googleapis.com/google.profile.Person",
+                              "firstName": <string>,
+                              "lastName": <string>
+                            }
+
+                        If the embedded message type is well-known and has a
+                        custom JSON
+
+                        representation, that representation will be embedded
+                        adding a field
+
+                        `value` which holds the custom JSON in addition to the
+                        `@type`
+
+                        field. Example (for message
+                        [google.protobuf.Duration][]):
+
+                            {
+                              "@type": "type.googleapis.com/google.protobuf.Duration",
+                              "value": "1.212s"
+                            }
+                      title: >-
+                        Address contains the data of the external chain address
+                        to be connected
+
+                        with the Desmos profile
+                    proof:
+                      title: >-
+                        Proof contains the ownership proof of the external chain
+                        address
+                      type: object
+                      properties:
+                        pub_key:
+                          type: object
+                          properties:
+                            type_url:
+                              type: string
+                              description: >-
+                                A URL/resource name that uniquely identifies the
+                                type of the serialized
+
+                                protocol buffer message. This string must
+                                contain at least
+
+                                one "/" character. The last segment of the URL's
+                                path must represent
+
+                                the fully qualified name of the type (as in
+
+                                `path/google.protobuf.Duration`). The name
+                                should be in a canonical form
+
+                                (e.g., leading "." is not accepted).
+
+
+                                In practice, teams usually precompile into the
+                                binary all types that they
+
+                                expect it to use in the context of Any. However,
+                                for URLs which use the
+
+                                scheme `http`, `https`, or no scheme, one can
+                                optionally set up a type
+
+                                server that maps type URLs to message
+                                definitions as follows:
+
+
+                                * If no scheme is provided, `https` is assumed.
+
+                                * An HTTP GET on the URL must yield a
+                                [google.protobuf.Type][]
+                                  value in binary format, or produce an error.
+                                * Applications are allowed to cache lookup
+                                results based on the
+                                  URL, or have them precompiled into a binary to avoid any
+                                  lookup. Therefore, binary compatibility needs to be preserved
+                                  on changes to types. (Use versioned type names to manage
+                                  breaking changes.)
+
+                                Note: this functionality is not currently
+                                available in the official
+
+                                protobuf release, and it is not used for type
+                                URLs beginning with
+
+                                type.googleapis.com.
+
+
+                                Schemes other than `http`, `https` (or the empty
+                                scheme) might be
+
+                                used with implementation specific semantics.
+                            value:
+                              type: string
+                              format: byte
+                              description: >-
+                                Must be a valid serialized protocol buffer of
+                                the above specified type.
+                          description: >-
+                            `Any` contains an arbitrary serialized protocol
+                            buffer message along with a
+
+                            URL that describes the type of the serialized
+                            message.
 
-                            {
-                              "@type": "type.googleapis.com/google.profile.Person",
-                              "firstName": <string>,
-                              "lastName": <string>
-                            }
 
-                        If the embedded message type is well-known and has a
-                        custom JSON
+                            Protobuf library provides support to pack/unpack Any
+                            values in the form
 
-                        representation, that representation will be embedded
-                        adding a field
+                            of utility functions or additional generated methods
+                            of the Any type.
 
-                        `value` which holds the custom JSON in addition to the
-                        `@type`
 
-                        field. Example (for message
-                        [google.protobuf.Duration][]):
+                            Example 1: Pack and unpack a message in C++.
 
-                            {
-                              "@type": "type.googleapis.com/google.protobuf.Duration",
-                              "value": "1.212s"
-                            }
-                      title: >-
-                        Address contains the data of the external chain address
-                        to be connected
+                                Foo foo = ...;
+                                Any any;
+                                any.PackFrom(foo);
+                                ...
+                                if (any.UnpackTo(&foo)) {
+                                  ...
+                                }
 
-                        with the Desmos profile
-                    proof:
-                      title: >-
-                        Proof contains the ownership proof of the external chain
-                        address
-                      type: object
-                      properties:
-                        pub_key:
+                            Example 2: Pack and unpack a message in Java.
+
+                                Foo foo = ...;
+                                Any any = Any.pack(foo);
+                                ...
+                                if (any.is(Foo.class)) {
+                                  foo = any.unpack(Foo.class);
+                                }
+
+                             Example 3: Pack and unpack a message in Python.
+
+                                foo = Foo(...)
+                                any = Any()
+                                any.Pack(foo)
+                                ...
+                                if any.Is(Foo.DESCRIPTOR):
+                                  any.Unpack(foo)
+                                  ...
+
+                             Example 4: Pack and unpack a message in Go
+
+                                 foo := &pb.Foo{...}
+                                 any, err := ptypes.MarshalAny(foo)
+                                 ...
+                                 foo := &pb.Foo{}
+                                 if err := ptypes.UnmarshalAny(any, foo); err != nil {
+                                   ...
+                                 }
+
+                            The pack methods provided by protobuf library will
+                            by default use
+
+                            'type.googleapis.com/full.type.name' as the type URL
+                            and the unpack
+
+                            methods only use the fully qualified type name after
+                            the last '/'
+
+                            in the type URL, for example "foo.bar.com/x/y.z"
+                            will yield type
+
+                            name "y.z".
+
+
+
+                            JSON
+
+                            ====
+
+                            The JSON representation of an `Any` value uses the
+                            regular
+
+                            representation of the deserialized, embedded
+                            message, with an
+
+                            additional field `@type` which contains the type
+                            URL. Example:
+
+                                package google.profile;
+                                message Person {
+                                  string first_name = 1;
+                                  string last_name = 2;
+                                }
+
+                                {
+                                  "@type": "type.googleapis.com/google.profile.Person",
+                                  "firstName": <string>,
+                                  "lastName": <string>
+                                }
+
+                            If the embedded message type is well-known and has a
+                            custom JSON
+
+                            representation, that representation will be embedded
+                            adding a field
+
+                            `value` which holds the custom JSON in addition to
+                            the `@type`
+
+                            field. Example (for message
+                            [google.protobuf.Duration][]):
+
+                                {
+                                  "@type": "type.googleapis.com/google.protobuf.Duration",
+                                  "value": "1.212s"
+                                }
+                          title: >-
+                            PubKey represents the public key associated with the
+                            address for which to
+
+                            prove the ownership
+                        signature:
                           type: object
                           properties:
                             type_url:
@@ -1111,258 +1622,412 @@ paths:
 
                             ====
 
-                            The JSON representation of an `Any` value uses the
-                            regular
+                            The JSON representation of an `Any` value uses the
+                            regular
+
+                            representation of the deserialized, embedded
+                            message, with an
+
+                            additional field `@type` which contains the type
+                            URL. Example:
+
+                                package google.profile;
+                                message Person {
+                                  string first_name = 1;
+                                  string last_name = 2;
+                                }
+
+                                {
+                                  "@type": "type.googleapis.com/google.profile.Person",
+                                  "firstName": <string>,
+                                  "lastName": <string>
+                                }
+
+                            If the embedded message type is well-known and has a
+                            custom JSON
+
+                            representation, that representation will be embedded
+                            adding a field
+
+                            `value` which holds the custom JSON in addition to
+                            the `@type`
+
+                            field. Example (for message
+                            [google.protobuf.Duration][]):
+
+                                {
+                                  "@type": "type.googleapis.com/google.protobuf.Duration",
+                                  "value": "1.212s"
+                                }
+                          title: >-
+                            Signature represents the hex-encoded signature of
+                            the PlainText value
+                        plain_text:
+                          type: string
+                          title: >-
+                            PlainText represents the hex-encoded value signed in
+                            order to produce the
+
+                            Signature
+                    chain_config:
+                      title: >-
+                        ChainConfig contains the configuration of the external
+                        chain
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                      description: >-
+                        ChainConfig contains the data of the chain with which
+                        the link is made.
+                    creation_time:
+                      type: string
+                      format: date-time
+                      title: >-
+                        CreationTime represents the time in which the link has
+                        been created
+                  title: >-
+                    ChainLink contains the data representing either an inter- or
+                    cross- chain
+
+                    link
+              pagination:
+                title: Pagination defines the pagination response
+                type: object
+                properties:
+                  next_key:
+                    type: string
+                    format: byte
+                    title: |-
+                      next_key is the key to be passed to PageRequest.key to
+                      query the next page most efficiently
+                  total:
+                    type: string
+                    format: uint64
+                    title: >-
+                      total is total number of results available if
+                      PageRequest.count_total
+
+                      was set, its value is undefined otherwise
+                description: >-
+                  PageResponse is to be embedded in gRPC response messages where
+                  the
+
+                  corresponding request message has used PageRequest.
+
+                   message SomeResponse {
+                           repeated Bar results = 1;
+                           PageResponse page = 2;
+                   }
+            description: |-
+              QueryChainLinksResponse is the response type for the
+              Query/ChainLinks RPC method.
+        default:
+          description: An unexpected error response
+          schema:
+            type: object
+            properties:
+              error:
+                type: string
+              code:
+                type: integer
+                format: int32
+              message:
+                type: string
+              details:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    type_url:
+                      type: string
+                      description: >-
+                        A URL/resource name that uniquely identifies the type of
+                        the serialized
+
+                        protocol buffer message. This string must contain at
+                        least
 
-                            representation of the deserialized, embedded
-                            message, with an
+                        one "/" character. The last segment of the URL's path
+                        must represent
 
-                            additional field `@type` which contains the type
-                            URL. Example:
+                        the fully qualified name of the type (as in
 
-                                package google.profile;
-                                message Person {
-                                  string first_name = 1;
-                                  string last_name = 2;
-                                }
+                        `path/google.protobuf.Duration`). The name should be in
+                        a canonical form
 
-                                {
-                                  "@type": "type.googleapis.com/google.profile.Person",
-                                  "firstName": <string>,
-                                  "lastName": <string>
-                                }
+                        (e.g., leading "." is not accepted).
 
-                            If the embedded message type is well-known and has a
-                            custom JSON
 
-                            representation, that representation will be embedded
-                            adding a field
+                        In practice, teams usually precompile into the binary
+                        all types that they
 
-                            `value` which holds the custom JSON in addition to
-                            the `@type`
+                        expect it to use in the context of Any. However, for
+                        URLs which use the
 
-                            field. Example (for message
-                            [google.protobuf.Duration][]):
+                        scheme `http`, `https`, or no scheme, one can optionally
+                        set up a type
 
-                                {
-                                  "@type": "type.googleapis.com/google.protobuf.Duration",
-                                  "value": "1.212s"
-                                }
-                          title: >-
-                            PubKey represents the public key associated with the
-                            address for which to
+                        server that maps type URLs to message definitions as
+                        follows:
 
-                            prove the ownership
-                        signature:
-                          type: object
-                          properties:
-                            type_url:
-                              type: string
-                              description: >-
-                                A URL/resource name that uniquely identifies the
-                                type of the serialized
 
-                                protocol buffer message. This string must
-                                contain at least
+                        * If no scheme is provided, `https` is assumed.
 
-                                one "/" character. The last segment of the URL's
-                                path must represent
+                        * An HTTP GET on the URL must yield a
+                        [google.protobuf.Type][]
+                          value in binary format, or produce an error.
+                        * Applications are allowed to cache lookup results based
+                        on the
+                          URL, or have them precompiled into a binary to avoid any
+                          lookup. Therefore, binary compatibility needs to be preserved
+                          on changes to types. (Use versioned type names to manage
+                          breaking changes.)
 
-                                the fully qualified name of the type (as in
+                        Note: this functionality is not currently available in
+                        the official
 
-                                `path/google.protobuf.Duration`). The name
-                                should be in a canonical form
+                        protobuf release, and it is not used for type URLs
+                        beginning with
 
-                                (e.g., leading "." is not accepted).
+                        type.googleapis.com.
 
 
-                                In practice, teams usually precompile into the
-                                binary all types that they
+                        Schemes other than `http`, `https` (or the empty scheme)
+                        might be
 
-                                expect it to use in the context of Any. However,
-                                for URLs which use the
+                        used with implementation specific semantics.
+                    value:
+                      type: string
+                      format: byte
+                      description: >-
+                        Must be a valid serialized protocol buffer of the above
+                        specified type.
+                  description: >-
+                    `Any` contains an arbitrary serialized protocol buffer
+                    message along with a
 
-                                scheme `http`, `https`, or no scheme, one can
-                                optionally set up a type
+                    URL that describes the type of the serialized message.
 
-                                server that maps type URLs to message
-                                definitions as follows:
 
+                    Protobuf library provides support to pack/unpack Any values
+                    in the form
 
-                                * If no scheme is provided, `https` is assumed.
+                    of utility functions or additional generated methods of the
+                    Any type.
 
-                                * An HTTP GET on the URL must yield a
-                                [google.protobuf.Type][]
-                                  value in binary format, or produce an error.
-                                * Applications are allowed to cache lookup
-                                results based on the
-                                  URL, or have them precompiled into a binary to avoid any
-                                  lookup. Therefore, binary compatibility needs to be preserved
-                                  on changes to types. (Use versioned type names to manage
-                                  breaking changes.)
 
-                                Note: this functionality is not currently
-                                available in the official
+                    Example 1: Pack and unpack a message in C++.
 
-                                protobuf release, and it is not used for type
-                                URLs beginning with
+                        Foo foo = ...;
+                        Any any;
+                        any.PackFrom(foo);
+                        ...
+                        if (any.UnpackTo(&foo)) {
+                          ...
+                        }
 
-                                type.googleapis.com.
+                    Example 2: Pack and unpack a message in Java.
 
+                        Foo foo = ...;
+                        Any any = Any.pack(foo);
+                        ...
+                        if (any.is(Foo.class)) {
+                          foo = any.unpack(Foo.class);
+                        }
 
-                                Schemes other than `http`, `https` (or the empty
-                                scheme) might be
+                     Example 3: Pack and unpack a message in Python.
 
-                                used with implementation specific semantics.
-                            value:
-                              type: string
-                              format: byte
-                              description: >-
-                                Must be a valid serialized protocol buffer of
-                                the above specified type.
-                          description: >-
-                            `Any` contains an arbitrary serialized protocol
-                            buffer message along with a
+                        foo = Foo(...)
+                        any = Any()
+                        any.Pack(foo)
+                        ...
+                        if any.Is(Foo.DESCRIPTOR):
+                          any.Unpack(foo)
+                          ...
 
-                            URL that describes the type of the serialized
-                            message.
+                     Example 4: Pack and unpack a message in Go
 
+                         foo := &pb.Foo{...}
+                         any, err := ptypes.MarshalAny(foo)
+                         ...
+                         foo := &pb.Foo{}
+                         if err := ptypes.UnmarshalAny(any, foo); err != nil {
+                           ...
+                         }
 
-                            Protobuf library provides support to pack/unpack Any
-                            values in the form
+                    The pack methods provided by protobuf library will by
+                    default use
 
-                            of utility functions or additional generated methods
-                            of the Any type.
+                    'type.googleapis.com/full.type.name' as the type URL and the
+                    unpack
 
+                    methods only use the fully qualified type name after the
+                    last '/'
 
-                            Example 1: Pack and unpack a message in C++.
+                    in the type URL, for example "foo.bar.com/x/y.z" will yield
+                    type
 
-                                Foo foo = ...;
-                                Any any;
-                                any.PackFrom(foo);
-                                ...
-                                if (any.UnpackTo(&foo)) {
-                                  ...
-                                }
+                    name "y.z".
 
-                            Example 2: Pack and unpack a message in Java.
 
-                                Foo foo = ...;
-                                Any any = Any.pack(foo);
-                                ...
-                                if (any.is(Foo.class)) {
-                                  foo = any.unpack(Foo.class);
-                                }
 
-                             Example 3: Pack and unpack a message in Python.
+                    JSON
 
-                                foo = Foo(...)
-                                any = Any()
-                                any.Pack(foo)
-                                ...
-                                if any.Is(Foo.DESCRIPTOR):
-                                  any.Unpack(foo)
-                                  ...
+                    ====
 
-                             Example 4: Pack and unpack a message in Go
+                    The JSON representation of an `Any` value uses the regular
 
-                                 foo := &pb.Foo{...}
-                                 any, err := ptypes.MarshalAny(foo)
-                                 ...
-                                 foo := &pb.Foo{}
-                                 if err := ptypes.UnmarshalAny(any, foo); err != nil {
-                                   ...
-                                 }
+                    representation of the deserialized, embedded message, with
+                    an
 
-                            The pack methods provided by protobuf library will
-                            by default use
+                    additional field `@type` which contains the type URL.
+                    Example:
 
-                            'type.googleapis.com/full.type.name' as the type URL
-                            and the unpack
+                        package google.profile;
+                        message Person {
+                          string first_name = 1;
+                          string last_name = 2;
+                        }
 
-                            methods only use the fully qualified type name after
-                            the last '/'
+                        {
+                          "@type": "type.googleapis.com/google.profile.Person",
+                          "firstName": <string>,
+                          "lastName": <string>
+                        }
 
-                            in the type URL, for example "foo.bar.com/x/y.z"
-                            will yield type
+                    If the embedded message type is well-known and has a custom
+                    JSON
 
-                            name "y.z".
+                    representation, that representation will be embedded adding
+                    a field
 
+                    `value` which holds the custom JSON in addition to the
+                    `@type`
 
+                    field. Example (for message [google.protobuf.Duration][]):
 
-                            JSON
+                        {
+                          "@type": "type.googleapis.com/google.protobuf.Duration",
+                          "value": "1.212s"
+                        }
+      parameters:
+        - name: user
+          description: >-
+            (optional) User represents the Desmos address of the user to which
+            search
 
-                            ====
+            the link for.
+          in: query
+          required: false
+          type: string
+        - name: chain_name
+          description: >-
+            (optional) ChainName contains the name of the chain to which search
+            the
 
-                            The JSON representation of an `Any` value uses the
-                            regular
+            link for. Used only if user is also set.
+          in: query
+          required: false
+          type: string
+        - name: target
+          description: >-
+            (optional) Target must contain the external address to which query
+            the link
 
-                            representation of the deserialized, embedded
-                            message, with an
+            for. Used only if chain name is also set.
+          in: query
+          required: false
+          type: string
+        - name: pagination.key
+          description: |-
+            key is a value returned in PageResponse.next_key to begin
+            querying the next page most efficiently. Only one of offset or key
+            should be set.
+          in: query
+          required: false
+          type: string
+          format: byte
+        - name: pagination.offset
+          description: >-
+            offset is a numeric offset that can be used when key is unavailable.
 
-                            additional field `@type` which contains the type
-                            URL. Example:
+            It is less efficient than using key. Only one of offset or key
+            should
 
-                                package google.profile;
-                                message Person {
-                                  string first_name = 1;
-                                  string last_name = 2;
-                                }
+            be set.
+          in: query
+          required: false
+          type: string
+          format: uint64
+        - name: pagination.limit
+          description: >-
+            limit is the total number of results to be returned in the result
+            page.
 
-                                {
-                                  "@type": "type.googleapis.com/google.profile.Person",
-                                  "firstName": <string>,
-                                  "lastName": <string>
-                                }
+            If left empty it will default to a value to be set by each app.
+          in: query
+          required: false
+          type: string
+          format: uint64
+        - name: pagination.count_total
+          description: >-
+            count_total is set to true  to indicate that the result set should
+            include
 
-                            If the embedded message type is well-known and has a
-                            custom JSON
+            a count of the total number of items available for pagination in
+            UIs.
 
-                            representation, that representation will be embedded
-                            adding a field
+            count_total is only respected when offset is used. It is ignored
+            when key
 
-                            `value` which holds the custom JSON in addition to
-                            the `@type`
+            is set.
+          in: query
+          required: false
+          type: boolean
+          format: boolean
+        - name: pagination.reverse
+          description: >-
+            reverse is set to true if results are to be returned in the
+            descending order.
 
-                            field. Example (for message
-                            [google.protobuf.Duration][]):
 
-                                {
-                                  "@type": "type.googleapis.com/google.protobuf.Duration",
-                                  "value": "1.212s"
-                                }
-                          title: >-
-                            Signature represents the hex-encoded signature of
-                            the PlainText value
-                        plain_text:
-                          type: string
-                          title: >-
-                            PlainText represents the hex-encoded value signed in
-                            order to produce the
+            Since: cosmos-sdk 0.43
+          in: query
+          required: false
+          type: boolean
+          format: boolean
+      tags:
+        - Query
+  /desmos/profiles/v2/chain-links/owners:
+    get:
+      summary: >-
+        ChainLinkOwners queries for the owners of chain links, optionally
+        searching
 
-                            Signature
-                    chain_config:
-                      title: >-
-                        ChainConfig contains the configuration of the external
-                        chain
-                      type: object
-                      properties:
-                        name:
-                          type: string
-                      description: >-
-                        ChainConfig contains the data of the chain with which
-                        the link is made.
-                    creation_time:
+        for a specific chain name and external address
+      operationId: ChainLinkOwners
+      responses:
+        '200':
+          description: A successful response.
+          schema:
+            type: object
+            properties:
+              owners:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    user:
+                      type: string
+                    chain_name:
+                      type: string
+                    target:
                       type: string
-                      format: date-time
-                      title: >-
-                        CreationTime represents the time in which the link has
-                        been created
                   title: >-
-                    ChainLink contains the data representing either an inter- or
-                    cross- chain
-
-                    link
+                    ChainLinkOwnerDetails contains the details of a single chain
+                    link owner
+                title: Addresses of the chain links owners
               pagination:
                 title: Pagination defines the pagination response
                 type: object
@@ -1391,9 +2056,11 @@ paths:
                            repeated Bar results = 1;
                            PageResponse page = 2;
                    }
-            description: |-
-              QueryChainLinksResponse is the response type for the
-              Query/ChainLinks RPC method.
+            description: >-
+              QueryChainLinkOwnersResponse contains the data returned by the
+              request
+
+              allowing to get chain link owners.
         default:
           description: An unexpected error response
           schema:
@@ -1584,30 +2251,21 @@ paths:
                           "value": "1.212s"
                         }
       parameters:
-        - name: user
-          description: >-
-            (optional) User represents the Desmos address of the user to which
-            search
-
-            the link for.
-          in: query
-          required: false
-          type: string
         - name: chain_name
           description: >-
-            (optional) ChainName contains the name of the chain to which search
-            the
+            (Optional) Chain name to search link owners of. If not specified,
+            all
 
-            link for. Used only if user is also set.
+            links stored will be searched instead.
           in: query
           required: false
           type: string
         - name: target
           description: >-
-            (optional) Target must contain the external address to which query
-            the link
+            (Optional) External address to search for. This will only be used if
+            the
 
-            for. Used only if chain name is also set.
+            chain name is specified as well.
           in: query
           required: false
           type: string
@@ -5539,6 +6197,69 @@ definitions:
     title: |-
       QueryApplicationLinkByClientIDResponse contains the data returned by the
       request allowing to get an application link using a client id
+  desmos.profiles.v2.QueryApplicationLinkOwnersResponse:
+    type: object
+    properties:
+      owners:
+        type: array
+        items:
+          type: object
+          properties:
+            user:
+              type: string
+            application:
+              type: string
+            username:
+              type: string
+          title: >-
+            ApplicationLinkOwnerDetails contains the details of a single
+            application
+
+            link owner
+        title: Addresses of the application links owners
+      pagination:
+        title: Pagination defines the pagination response
+        type: object
+        properties:
+          next_key:
+            type: string
+            format: byte
+            title: |-
+              next_key is the key to be passed to PageRequest.key to
+              query the next page most efficiently
+          total:
+            type: string
+            format: uint64
+            title: >-
+              total is total number of results available if
+              PageRequest.count_total
+
+              was set, its value is undefined otherwise
+        description: |-
+          PageResponse is to be embedded in gRPC response messages where the
+          corresponding request message has used PageRequest.
+
+           message SomeResponse {
+                   repeated Bar results = 1;
+                   PageResponse page = 2;
+           }
+    description: >-
+      QueryApplicationLinkOwnersResponse contains the data returned by the
+      request
+
+      allowing to get application link owners.
+  desmos.profiles.v2.QueryApplicationLinkOwnersResponse.ApplicationLinkOwnerDetails:
+    type: object
+    properties:
+      user:
+        type: string
+      application:
+        type: string
+      username:
+        type: string
+    title: |-
+      ApplicationLinkOwnerDetails contains the details of a single application
+      link owner
   desmos.profiles.v2.QueryApplicationLinksResponse:
     type: object
     properties:
@@ -5677,6 +6398,63 @@ definitions:
     title: |-
       QueryApplicationLinksResponse represents the response to the query used
       to get the application links for a specific user
+  desmos.profiles.v2.QueryChainLinkOwnersResponse:
+    type: object
+    properties:
+      owners:
+        type: array
+        items:
+          type: object
+          properties:
+            user:
+              type: string
+            chain_name:
+              type: string
+            target:
+              type: string
+          title: >-
+            ChainLinkOwnerDetails contains the details of a single chain link
+            owner
+        title: Addresses of the chain links owners
+      pagination:
+        title: Pagination defines the pagination response
+        type: object
+        properties:
+          next_key:
+            type: string
+            format: byte
+            title: |-
+              next_key is the key to be passed to PageRequest.key to
+              query the next page most efficiently
+          total:
+            type: string
+            format: uint64
+            title: >-
+              total is total number of results available if
+              PageRequest.count_total
+
+              was set, its value is undefined otherwise
+        description: |-
+          PageResponse is to be embedded in gRPC response messages where the
+          corresponding request message has used PageRequest.
+
+           message SomeResponse {
+                   repeated Bar results = 1;
+                   PageResponse page = 2;
+           }
+    description: |-
+      QueryChainLinkOwnersResponse contains the data returned by the request
+      allowing to get chain link owners.
+  desmos.profiles.v2.QueryChainLinkOwnersResponse.ChainLinkOwnerDetails:
+    type: object
+    properties:
+      user:
+        type: string
+      chain_name:
+        type: string
+      target:
+        type: string
+    title: ChainLinkOwnerDetails contains the details of a single chain link owner
   desmos.profiles.v2.QueryChainLinksResponse:
     type: object
     properties:
diff --git a/proto/desmos/profiles/v2/query.proto b/proto/desmos/profiles/v2/query.proto
index 7ef300ed..7a5d7d3b 100644
--- a/proto/desmos/profiles/v2/query.proto
+++ b/proto/desmos/profiles/v2/query.proto
@@ -36,6 +36,13 @@ service Query {
     option (google.api.http).get = "/desmos/profiles/v2/chain-links";
   }
 
+  // ChainLinkOwners queries for the owners of chain links, optionally searching
+  // for a specific chain name and external address
+  rpc ChainLinkOwners(QueryChainLinkOwnersRequest)
+      returns (QueryChainLinkOwnersResponse) {
+    option (google.api.http).get = "/desmos/profiles/v2/chain-links/owners";
+  }
+
   // ApplicationLinks queries the applications links associated to the given
   // user, if provided. Otherwise, it queries all the application links stored.
   rpc ApplicationLinks(QueryApplicationLinksRequest)
@@ -51,6 +58,13 @@ service Query {
         "/desmos/profiles/v2/app-links/clients/{client_id}";
   }
 
+  // ApplicationLinkOwners queries for the owners of applications links,
+  // optionally searching for a specific application and username.
+  rpc ApplicationLinkOwners(QueryApplicationLinkOwnersRequest)
+      returns (QueryApplicationLinkOwnersResponse) {
+    option (google.api.http).get = "/desmos/profiles/v2/app-links/owners";
+  }
+
   // Params queries the profiles module params
   rpc Params(QueryParamsRequest) returns (QueryParamsResponse) {
     option (google.api.http).get = "/desmos/profiles/v2/params";
diff --git a/proto/desmos/profiles/v2/query_app_links.proto b/proto/desmos/profiles/v2/query_app_links.proto
index 9f8808b7..eb860f41 100644
--- a/proto/desmos/profiles/v2/query_app_links.proto
+++ b/proto/desmos/profiles/v2/query_app_links.proto
@@ -49,4 +49,38 @@ message QueryApplicationLinkByClientIDRequest {
 // request allowing to get an application link using a client id
 message QueryApplicationLinkByClientIDResponse {
   ApplicationLink link = 1 [ (gogoproto.nullable) = false ];
+}
+
+// QueryApplicationLinkOwnersRequest contains the data of the request that can
+// be used to get application link owners
+message QueryApplicationLinkOwnersRequest {
+  // (Optional) Application name to search link owners of. If not specified, all
+  // links stored will be searched instead.
+  string application = 1;
+
+  // (Optional) Username to search for. This will only be used if the
+  // application is specified as well
+  string username = 2;
+
+  // Pagination defines an optional pagination for the request
+  cosmos.base.query.v1beta1.PageRequest pagination = 3;
+}
+
+// QueryApplicationLinkOwnersResponse contains the data returned by the request
+// allowing to get application link owners.
+message QueryApplicationLinkOwnersResponse {
+  // ApplicationLinkOwnerDetails contains the details of a single application
+  // link owner
+  message ApplicationLinkOwnerDetails {
+    string user = 1;
+    string application = 2;
+    string username = 3;
+  }
+
+  // Addresses of the application links owners
+  repeated ApplicationLinkOwnerDetails owners = 1
+      [ (gogoproto.nullable) = false ];
+
+  // Pagination defines the pagination response
+  cosmos.base.query.v1beta1.PageResponse pagination = 2;
 }
\ No newline at end of file
diff --git a/proto/desmos/profiles/v2/query_chain_links.proto b/proto/desmos/profiles/v2/query_chain_links.proto
index e7898b37..336ce729 100644
--- a/proto/desmos/profiles/v2/query_chain_links.proto
+++ b/proto/desmos/profiles/v2/query_chain_links.proto
@@ -38,3 +38,35 @@ message QueryChainLinksResponse {
   // Pagination defines the pagination response
   cosmos.base.query.v1beta1.PageResponse pagination = 2;
 }
+
+// QueryChainLinkOwnersRequest contains the data of the request that can
+// be used to get chain link owners
+message QueryChainLinkOwnersRequest {
+  // (Optional) Chain name to search link owners of. If not specified, all
+  // links stored will be searched instead.
+  string chain_name = 1;
+
+  // (Optional) External address to search for. This will only be used if the
+  // chain name is specified as well
+  string target = 2;
+
+  // Pagination defines an optional pagination for the request
+  cosmos.base.query.v1beta1.PageRequest pagination = 3;
+}
+
+// QueryChainLinkOwnersResponse contains the data returned by the request
+// allowing to get chain link owners.
+message QueryChainLinkOwnersResponse {
+  // ChainLinkOwnerDetails contains the details of a single chain link owner
+  message ChainLinkOwnerDetails {
+    string user = 1;
+    string chain_name = 2;
+    string target = 3;
+  }
+
+  // Addresses of the chain links owners
+  repeated ChainLinkOwnerDetails owners = 1 [ (gogoproto.nullable) = false ];
+
+  // Pagination defines the pagination response
+  cosmos.base.query.v1beta1.PageResponse pagination = 2;
+}
\ No newline at end of file
diff --git a/x/profiles/keeper/grpc_query.go b/x/profiles/keeper/grpc_query.go
index 5928229a..b4974c9e 100644
--- a/x/profiles/keeper/grpc_query.go
+++ b/x/profiles/keeper/grpc_query.go
@@ -122,6 +122,42 @@ func (k Keeper) ChainLinks(ctx context.Context, request *types.QueryChainLinksRe
 	return &types.QueryChainLinksResponse{Links: links, Pagination: pageRes}, nil
 }
 
+// ChainLinkOwners implements the Query/ChainLinkOwners gRPC method
+func (k Keeper) ChainLinkOwners(ctx context.Context, request *types.QueryChainLinkOwnersRequest) (*types.QueryChainLinkOwnersResponse, error) {
+	sdkCtx := sdk.UnwrapSDKContext(ctx)
+	store := sdkCtx.KVStore(k.storeKey)
+
+	ownersPrefix := types.ChainLinkChainPrefix
+	switch {
+	case request.ChainName != "" && request.Target != "":
+		ownersPrefix = types.ChainLinkChainAddressKey(request.ChainName, request.Target)
+	case request.ChainName != "":
+		ownersPrefix = types.ChainLinkChainKey(request.ChainName)
+	}
+
+	var owners []types.QueryChainLinkOwnersResponse_ChainLinkOwnerDetails
+	ownersStore := prefix.NewStore(store, ownersPrefix)
+	pageRes, err := query.Paginate(ownersStore, request.Pagination, func(key []byte, value []byte) error {
+		// Re-add the prefix because the prefix store trims it out, and we need it to get the data
+		keyWithPrefix := append(ownersPrefix, key...)
+		chainName, target, user := types.GetChainLinkOwnerData(keyWithPrefix)
+
+		owners = append(owners, types.QueryChainLinkOwnersResponse_ChainLinkOwnerDetails{
+			User:      user,
+			ChainName: chainName,
+			Target:    target,
+		})
+
+		return nil
+	})
+
+	if err != nil {
+		return nil, status.Error(codes.Internal, err.Error())
+	}
+
+	return &types.QueryChainLinkOwnersResponse{Owners: owners, Pagination: pageRes}, nil
+}
+
 // ApplicationLinks implements the Query/ApplicationLinks gRPC method
 func (k Keeper) ApplicationLinks(ctx context.Context, request *types.QueryApplicationLinksRequest) (*types.QueryApplicationLinksResponse, error) {
 	sdkCtx := sdk.UnwrapSDKContext(ctx)
@@ -174,6 +210,42 @@ func (k Keeper) ApplicationLinkByClientID(ctx context.Context, request *types.Qu
 	return &types.QueryApplicationLinkByClientIDResponse{Link: link}, nil
 }
 
+// ApplicationLinkOwners implements the Query/ApplicationLinkOwners gRPC method
+func (k Keeper) ApplicationLinkOwners(ctx context.Context, request *types.QueryApplicationLinkOwnersRequest) (*types.QueryApplicationLinkOwnersResponse, error) {
+	sdkCtx := sdk.UnwrapSDKContext(ctx)
+	store := sdkCtx.KVStore(k.storeKey)
+
+	ownersPrefix := types.ApplicationLinkAppPrefix
+	switch {
+	case request.Application != "" && request.Username != "":
+		ownersPrefix = types.ApplicationLinkAppUsernameKey(request.Application, request.Username)
+	case request.Application != "":
+		ownersPrefix = types.ApplicationLinkAppKey(request.Application)
+	}
+
+	var owners []types.QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails
+	ownersStore := prefix.NewStore(store, ownersPrefix)
+	pageRes, err := query.Paginate(ownersStore, request.Pagination, func(key []byte, value []byte) error {
+		// Re-add the prefix because the prefix store trims it out, and we need it to get the data
+		keyWithPrefix := append(ownersPrefix, key...)
+		application, username, user := types.GetApplicationLinkOwnerData(keyWithPrefix)
+
+		owners = append(owners, types.QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails{
+			User:        user,
+			Application: application,
+			Username:    username,
+		})
+
+		return nil
+	})
+
+	if err != nil {
+		return nil, status.Error(codes.Internal, err.Error())
+	}
+
+	return &types.QueryApplicationLinkOwnersResponse{Owners: owners, Pagination: pageRes}, nil
+}
+
 // Params implements the Query/Params gRPC method
 func (k Keeper) Params(ctx context.Context, _ *types.QueryParamsRequest) (*types.QueryParamsResponse, error) {
 	sdkCtx := sdk.UnwrapSDKContext(ctx)
diff --git a/x/profiles/keeper/keeper_app_links.go b/x/profiles/keeper/keeper_app_links.go
index 97a56b3c..c54e891a 100644
--- a/x/profiles/keeper/keeper_app_links.go
+++ b/x/profiles/keeper/keeper_app_links.go
@@ -10,6 +10,7 @@ import (
 // Connections are stored using three keys:
 // 1. UserApplicationLinkKey (user + application + username)  -> types.ApplicationLink
 // 2. ApplicationLinkClientIDKey (client_id)                  -> UserApplicationLinkKey
+// 3. ApplicationLinkOwnerKey (application + username + user) -> 0x01
 //
 // This allows to get connections by client id as well as by app + username quickly
 
@@ -19,14 +20,12 @@ func (k Keeper) SaveApplicationLink(ctx sdk.Context, link types.ApplicationLink)
 		return sdkerrors.Wrapf(types.ErrProfileNotFound, "a profile is required to link an application")
 	}
 
-	// Get the keys
-	userApplicationLinkKey := types.UserApplicationLinkKey(link.User, link.Data.Application, link.Data.Username)
-	applicationLinkClientIDKey := types.ApplicationLinkClientIDKey(link.OracleRequest.ClientID)
-
 	// Store the data
 	store := ctx.KVStore(k.storeKey)
+	userApplicationLinkKey := types.UserApplicationLinkKey(link.User, link.Data.Application, link.Data.Username)
 	store.Set(userApplicationLinkKey, types.MustMarshalApplicationLink(k.cdc, link))
-	store.Set(applicationLinkClientIDKey, userApplicationLinkKey)
+	store.Set(types.ApplicationLinkClientIDKey(link.OracleRequest.ClientID), userApplicationLinkKey)
+	store.Set(types.ApplicationLinkOwnerKey(link.Data.Application, link.Data.Username, link.User), []byte{0x01})
 
 	ctx.EventManager().EmitEvent(
 		sdk.NewEvent(
@@ -98,6 +97,7 @@ func (k Keeper) DeleteApplicationLink(ctx sdk.Context, appLink types.Application
 	store := ctx.KVStore(k.storeKey)
 	store.Delete(types.UserApplicationLinkKey(appLink.User, appLink.Data.Application, appLink.Data.Username))
 	store.Delete(types.ApplicationLinkClientIDKey(appLink.OracleRequest.ClientID))
+	store.Delete(types.ApplicationLinkOwnerKey(appLink.Data.Application, appLink.Data.Username, appLink.User))
 
 	k.AfterApplicationLinkDeleted(ctx, appLink)
 }
diff --git a/x/profiles/keeper/keeper_chain_links.go b/x/profiles/keeper/keeper_chain_links.go
index 3f7bcd94..42b207ef 100644
--- a/x/profiles/keeper/keeper_chain_links.go
+++ b/x/profiles/keeper/keeper_chain_links.go
@@ -8,6 +8,9 @@ import (
 )
 
 // SaveChainLink stores the given chain link
+// Chain links are stored using two keys:
+// 1. ChainLinkStoreKey (user + chain name + target) -> types.ChainLink
+// 2. ChainLinkOwnerKey (chain name + target + user) -> 0x01
 func (k Keeper) SaveChainLink(ctx sdk.Context, link types.ChainLink) error {
 	// Validate the chain link
 	err := link.Validate()
@@ -42,10 +45,10 @@ func (k Keeper) SaveChainLink(ctx sdk.Context, link types.ChainLink) error {
 		return types.ErrDuplicatedChainLink
 	}
 
-	// Set chain link -> address association
+	// Store the data
 	store := ctx.KVStore(k.storeKey)
-	key := types.ChainLinksStoreKey(link.User, link.ChainConfig.Name, target)
-	store.Set(key, types.MustMarshalChainLink(k.cdc, link))
+	store.Set(types.ChainLinksStoreKey(link.User, link.ChainConfig.Name, target), types.MustMarshalChainLink(k.cdc, link))
+	store.Set(types.ChainLinkOwnerKey(link.ChainConfig.Name, target, link.User), []byte{0x01})
 
 	k.AfterChainLinkSaved(ctx, link)
 
@@ -75,6 +78,7 @@ func (k Keeper) GetChainLink(ctx sdk.Context, owner, chainName, target string) (
 func (k Keeper) DeleteChainLink(ctx sdk.Context, link types.ChainLink) {
 	store := ctx.KVStore(k.storeKey)
 	store.Delete(types.ChainLinksStoreKey(link.User, link.ChainConfig.Name, link.GetAddressData().GetValue()))
+	store.Delete(types.ChainLinkOwnerKey(link.ChainConfig.Name, link.GetAddressData().GetValue(), link.User))
 
 	k.AfterChainLinkDeleted(ctx, link)
 }
diff --git a/x/profiles/keeper/migrations.go b/x/profiles/keeper/migrations.go
index d59ce21d..d50ea40d 100644
--- a/x/profiles/keeper/migrations.go
+++ b/x/profiles/keeper/migrations.go
@@ -7,6 +7,7 @@ import (
 	"github.com/gogo/protobuf/grpc"
 
 	v4 "github.com/desmos-labs/desmos/v3/x/profiles/legacy/v4"
+	v5 "github.com/desmos-labs/desmos/v3/x/profiles/legacy/v5"
 )
 
 // DONTCOVER
@@ -33,3 +34,8 @@ func NewMigrator(ak authkeeper.AccountKeeper, keeper Keeper, amino *codec.Legacy
 func (m Migrator) Migrate4to5(ctx sdk.Context) error {
 	return v4.MigrateStore(ctx, m.ak, m.keeper.storeKey, m.amino, m.keeper.cdc)
 }
+
+// Migrate5To6 migrates from version 5 to 6.
+func (m Migrator) Migrate5To6(ctx sdk.Context) error {
+	return v5.MigrateStore(ctx, m.keeper.storeKey, m.keeper.cdc)
+}
diff --git a/x/profiles/legacy/v5/store.go b/x/profiles/legacy/v5/store.go
new file mode 100644
index 00000000..0bef62a8
--- /dev/null
+++ b/x/profiles/legacy/v5/store.go
@@ -0,0 +1,83 @@
+package v5
+
+import (
+	"github.com/cosmos/cosmos-sdk/codec"
+	"github.com/cosmos/cosmos-sdk/store/prefix"
+	sdk "github.com/cosmos/cosmos-sdk/types"
+
+	"github.com/desmos-labs/desmos/v3/x/profiles/types"
+)
+
+// MigrateStore performs in-place store migrations from v5 to v6
+// The migration includes:
+//
+// - add missing application links owner keys
+// - add missing chain links owner keys
+//
+func MigrateStore(ctx sdk.Context, storeKey sdk.StoreKey, cdc codec.BinaryCodec) error {
+	store := ctx.KVStore(storeKey)
+
+	// Fix the application links
+	err := fixApplicationLinks(store, cdc)
+	if err != nil {
+		return err
+	}
+
+	// Fix the chain links
+	err = fixChainLinks(store, cdc)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// fixApplicationLinks fixes the application links by adding the missing owner keys
+func fixApplicationLinks(store sdk.KVStore, cdc codec.BinaryCodec) error {
+	applicationLinksStore := prefix.NewStore(store, types.ApplicationLinkPrefix)
+	applicationLinksIterator := applicationLinksStore.Iterator(nil, nil)
+
+	var applicationLinks []types.ApplicationLink
+	for ; applicationLinksIterator.Valid(); applicationLinksIterator.Next() {
+		var applicationLink types.ApplicationLink
+		err := cdc.Unmarshal(applicationLinksIterator.Value(), &applicationLink)
+		if err != nil {
+			return err
+		}
+
+		applicationLinks = append(applicationLinks, applicationLink)
+	}
+
+	applicationLinksIterator.Close()
+
+	for _, link := range applicationLinks {
+		store.Set(types.ApplicationLinkOwnerKey(link.Data.Application, link.Data.Username, link.User), []byte{0x01})
+	}
+
+	return nil
+}
+
+// fixChainLinks fixes the chain links by adding the missing owner keys
+func fixChainLinks(store sdk.KVStore, cdc codec.BinaryCodec) error {
+	chainLinkStore := prefix.NewStore(store, types.ChainLinksPrefix)
+	chainLinksIterator := chainLinkStore.Iterator(nil, nil)
+
+	var chainLinks []types.ChainLink
+	for ; chainLinksIterator.Valid(); chainLinksIterator.Next() {
+		var chainLink types.ChainLink
+		err := cdc.Unmarshal(chainLinksIterator.Value(), &chainLink)
+		if err != nil {
+			return err
+		}
+
+		chainLinks = append(chainLinks, chainLink)
+	}
+
+	chainLinksIterator.Close()
+
+	for _, link := range chainLinks {
+		store.Set(types.ChainLinkOwnerKey(link.ChainConfig.Name, link.GetAddressData().GetValue(), link.User), []byte{0x01})
+	}
+
+	return nil
+}
diff --git a/x/profiles/module.go b/x/profiles/module.go
index 2074e518..35ddb452 100644
--- a/x/profiles/module.go
+++ b/x/profiles/module.go
@@ -28,7 +28,7 @@ import (
 )
 
 const (
-	consensusVersion = 5
+	consensusVersion = 6
 )
 
 // type check to ensure the interface is properly implemented
@@ -112,6 +112,10 @@ func (am AppModule) RegisterServices(cfg module.Configurator) {
 	if err != nil {
 		panic(err)
 	}
+	err = cfg.RegisterMigration(types.ModuleName, 5, m.Migrate5To6)
+	if err != nil {
+		panic(err)
+	}
 }
 
 // NewAppModule creates a new AppModule Object
diff --git a/x/profiles/types/keys.go b/x/profiles/types/keys.go
index 53b613d9..d8663847 100644
--- a/x/profiles/types/keys.go
+++ b/x/profiles/types/keys.go
@@ -1,6 +1,7 @@
 package types
 
 import (
+	"bytes"
 	"strings"
 )
 
@@ -32,6 +33,8 @@ const (
 )
 
 var (
+	Separator = []byte{0x00}
+
 	// IBCPortKey defines the key to store the port ID in store
 	IBCPortKey = []byte{0x01}
 
@@ -40,6 +43,9 @@ var (
 	ChainLinksPrefix              = []byte{0x12}
 	ApplicationLinkPrefix         = []byte{0x13}
 	ApplicationLinkClientIDPrefix = []byte{0x14}
+
+	ChainLinkChainPrefix     = []byte{0x15}
+	ApplicationLinkAppPrefix = []byte{0x16}
 )
 
 // DTagStoreKey turns a DTag into the key used to store the address associated with it into the store
@@ -74,6 +80,28 @@ func ChainLinksStoreKey(user, chainName, address string) []byte {
 	return append(UserChainLinksChainPrefix(user, chainName), []byte(address)...)
 }
 
+// ChainLinkChainKey returns the key used to store all the chain links associated to the chain with the given name
+func ChainLinkChainKey(chainName string) []byte {
+	return append(ChainLinkChainPrefix, []byte(chainName)...)
+}
+
+// ChainLinkChainAddressKey returns the key used to store all the links for the given chain and external address
+func ChainLinkChainAddressKey(chainName, address string) []byte {
+	return append(ChainLinkChainKey(chainName), append(Separator, []byte(address)...)...)
+}
+
+// ChainLinkOwnerKey returns the key to store the owner of the chain link to the given chain and external address
+func ChainLinkOwnerKey(chainName, target, owner string) []byte {
+	return append(ChainLinkChainAddressKey(chainName, target), append(Separator, []byte(owner)...)...)
+}
+
+// GetChainLinkOwnerData returns the application link chain name, target and owner from the given key
+func GetChainLinkOwnerData(key []byte) (chainName, target, owner string) {
+	cleanedKey := bytes.TrimPrefix(key, ChainLinkChainPrefix)
+	values := bytes.Split(cleanedKey, Separator)
+	return string(values[0]), string(values[1]), string(values[2])
+}
+
 // UserApplicationLinksPrefix returns the store prefix used to identify all the application links for the given user
 func UserApplicationLinksPrefix(user string) []byte {
 	return append(ApplicationLinkPrefix, []byte(user)...)
@@ -96,3 +124,28 @@ func UserApplicationLinkKey(user, application, username string) []byte {
 func ApplicationLinkClientIDKey(clientID string) []byte {
 	return append(ApplicationLinkClientIDPrefix, []byte(clientID)...)
 }
+
+// ApplicationLinkAppKey returns the key used to store all the application
+// links associated to the given application
+func ApplicationLinkAppKey(application string) []byte {
+	return append(ApplicationLinkAppPrefix, []byte(application)...)
+}
+
+// ApplicationLinkAppUsernameKey returns the key used to store all the application
+// links for the given application and username
+func ApplicationLinkAppUsernameKey(application, username string) []byte {
+	return append(ApplicationLinkAppKey(application), append(Separator, []byte(username)...)...)
+}
+
+// ApplicationLinkOwnerKey returns the key used to store the given owner associating it to the application link
+// having the provided application and username
+func ApplicationLinkOwnerKey(application, username, owner string) []byte {
+	return append(ApplicationLinkAppUsernameKey(application, username), append(Separator, []byte(owner)...)...)
+}
+
+// GetApplicationLinkOwnerData returns the application, username and owner from a given ApplicationLinkOwnerKey
+func GetApplicationLinkOwnerData(key []byte) (application, username, owner string) {
+	cleanedKey := bytes.TrimPrefix(key, ApplicationLinkAppPrefix)
+	values := bytes.Split(cleanedKey, Separator)
+	return string(values[0]), string(values[1]), string(values[2])
+}
diff --git a/x/profiles/types/query.pb.go b/x/profiles/types/query.pb.go
index 04442cff..c5cb0c9c 100644
--- a/x/profiles/types/query.pb.go
+++ b/x/profiles/types/query.pb.go
@@ -33,41 +33,46 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
 func init() { proto.RegisterFile("desmos/profiles/v2/query.proto", fileDescriptor_fba4df0d7bde4d7c) }
 
 var fileDescriptor_fba4df0d7bde4d7c = []byte{
-	// 544 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0x4f, 0x6f, 0xd3, 0x30,
-	0x14, 0xc0, 0x1b, 0x24, 0x36, 0x29, 0x27, 0x64, 0x71, 0x69, 0x54, 0x32, 0x31, 0xd4, 0x75, 0xea,
-	0xb6, 0x98, 0xb4, 0xa7, 0x21, 0x38, 0xb0, 0xed, 0x32, 0xc1, 0x61, 0xa0, 0x9d, 0xb8, 0x44, 0x4e,
-	0xea, 0x66, 0x16, 0xa9, 0xed, 0xc5, 0x4e, 0x45, 0x35, 0xf5, 0xc2, 0x8d, 0x0b, 0x42, 0xe2, 0xce,
-	0xe7, 0xe1, 0x82, 0x98, 0xc4, 0x85, 0x23, 0x6a, 0x11, 0x9f, 0x03, 0xc5, 0x7f, 0x56, 0x31, 0x92,
-	0xfd, 0xe9, 0xcd, 0xce, 0xfb, 0x3d, 0xbf, 0x9f, 0xed, 0x17, 0xbb, 0xfe, 0x00, 0x8b, 0x11, 0x13,
-	0x90, 0xe7, 0x6c, 0x48, 0x32, 0x2c, 0xe0, 0xb8, 0x07, 0x4f, 0x0b, 0x9c, 0x4f, 0x02, 0x9e, 0x33,
-	0xc9, 0x00, 0xd0, 0xf1, 0xc0, 0xc6, 0x83, 0x71, 0xcf, 0xbb, 0x9f, 0xb2, 0x94, 0xa9, 0x30, 0x2c,
-	0x47, 0x9a, 0xf4, 0x5a, 0x29, 0x63, 0x69, 0x86, 0x21, 0xe2, 0x04, 0x22, 0x4a, 0x99, 0x44, 0x92,
-	0x30, 0x2a, 0x4c, 0xb4, 0x69, 0xa2, 0x6a, 0x16, 0x17, 0x43, 0x88, 0xa8, 0x29, 0xe1, 0x6d, 0xd4,
-	0x29, 0x44, 0xe6, 0x8b, 0xe1, 0xb6, 0x6b, 0xb9, 0x81, 0x44, 0x69, 0x94, 0xe3, 0xd3, 0x02, 0x0b,
-	0x69, 0x0b, 0xb6, 0xeb, 0x57, 0x45, 0x39, 0x1a, 0x59, 0xac, 0x5b, 0x8b, 0x25, 0x27, 0x88, 0xd0,
-	0x28, 0x23, 0xf4, 0xad, 0x65, 0x37, 0x6b, 0x59, 0xc4, 0xf9, 0x3f, 0x64, 0x33, 0x61, 0x25, 0x19,
-	0xe9, 0x43, 0xd2, 0x13, 0x5b, 0x50, 0xcf, 0x60, 0x8c, 0x04, 0xd6, 0xd9, 0x70, 0x1c, 0xc6, 0x58,
-	0xa2, 0x10, 0x72, 0x94, 0x12, 0xaa, 0x4e, 0x4d, 0xb3, 0xbd, 0x3f, 0xab, 0xee, 0xdd, 0x57, 0x25,
-	0x02, 0x3e, 0x38, 0xee, 0xea, 0x91, 0x2e, 0x0b, 0x3a, 0xc1, 0xff, 0x77, 0x12, 0x28, 0xcc, 0x10,
-	0xaf, 0xf5, 0x49, 0x78, 0x9b, 0xd7, 0x83, 0x82, 0x33, 0x2a, 0xf0, 0xfa, 0xd6, 0xfb, 0x1f, 0xbf,
-	0x3f, 0xdf, 0x69, 0x83, 0x47, 0xb0, 0x62, 0x8b, 0x17, 0xe3, 0xb3, 0x42, 0xe0, 0x7c, 0x0a, 0xbe,
-	0x3b, 0x6e, 0xeb, 0x90, 0x26, 0x6c, 0x44, 0x68, 0x7a, 0x70, 0x8c, 0xd2, 0xe3, 0x1c, 0x51, 0x31,
-	0xc4, 0xb9, 0x29, 0x2b, 0xc0, 0xd3, 0xda, 0xba, 0x57, 0xa5, 0x59, 0xeb, 0x67, 0x4b, 0x66, 0x9b,
-	0xad, 0xf4, 0xd4, 0x56, 0xb6, 0x41, 0xb7, 0x6a, 0x2b, 0xaa, 0x51, 0xa4, 0x49, 0xbd, 0xe8, 0x18,
-	0xf0, 0xd1, 0x71, 0xdd, 0xfd, 0xf2, 0xba, 0x5f, 0x96, 0x77, 0x08, 0xba, 0xb5, 0x06, 0x0b, 0xc8,
-	0xda, 0x6e, 0xdd, 0x88, 0x35, 0x6e, 0x1d, 0xe5, 0xf6, 0x10, 0xac, 0x55, 0xb9, 0xa9, 0x7e, 0xdb,
-	0x51, 0x5d, 0x04, 0xbe, 0x38, 0xee, 0xbd, 0xe7, 0x9c, 0x67, 0x24, 0x51, 0xed, 0xa0, 0xb5, 0x1e,
-	0xd7, 0x96, 0xba, 0x8c, 0x5a, 0xb9, 0xf0, 0x16, 0x19, 0x46, 0xb1, 0xad, 0x14, 0xd7, 0xc0, 0x83,
-	0x2a, 0x45, 0xc4, 0xb9, 0x11, 0xfc, 0xe6, 0xb8, 0xcd, 0x4b, 0x6b, 0xec, 0x4d, 0xf6, 0x33, 0x82,
-	0xa9, 0x3c, 0x3c, 0x00, 0xbb, 0x37, 0xad, 0xbb, 0xc8, 0xb1, 0xca, 0x4f, 0x96, 0x49, 0x35, 0xee,
-	0xbb, 0xca, 0xbd, 0x0f, 0xc2, 0x2b, 0xdd, 0x61, 0xa2, 0xf2, 0x04, 0x3c, 0xd3, 0x83, 0x88, 0x0c,
-	0xa6, 0x60, 0xea, 0xae, 0x1c, 0xa9, 0x67, 0x01, 0x6c, 0xd4, 0xff, 0x34, 0x0a, 0xb0, 0xa2, 0x9d,
-	0x6b, 0x39, 0x63, 0xb5, 0xae, 0xac, 0x5a, 0xc0, 0xab, 0xfc, 0xb7, 0x14, 0xbb, 0xf7, 0xe2, 0xeb,
-	0xcc, 0x77, 0xce, 0x67, 0xbe, 0xf3, 0x6b, 0xe6, 0x3b, 0x9f, 0xe6, 0x7e, 0xe3, 0x7c, 0xee, 0x37,
-	0x7e, 0xce, 0xfd, 0xc6, 0x9b, 0x30, 0x25, 0xf2, 0xa4, 0x88, 0x83, 0x84, 0x8d, 0x4c, 0xfe, 0x4e,
-	0x86, 0x62, 0x61, 0xd7, 0x1a, 0xf7, 0xe1, 0xbb, 0xc5, 0x82, 0x72, 0xc2, 0xb1, 0x88, 0x57, 0xd4,
-	0xe3, 0xd1, 0xff, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x1d, 0x87, 0x83, 0xf3, 0xdb, 0x05, 0x00, 0x00,
+	// 611 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0x4d, 0x6b, 0x13, 0x41,
+	0x18, 0xc7, 0xb3, 0x82, 0x15, 0xe6, 0xa2, 0x0c, 0x7a, 0x48, 0x88, 0x5b, 0xac, 0xe6, 0x85, 0x34,
+	0xdd, 0x31, 0x09, 0x0a, 0x15, 0x3d, 0xd8, 0xf6, 0x52, 0x14, 0xac, 0xd2, 0x93, 0x97, 0x30, 0xbb,
+	0x99, 0x6c, 0x07, 0x37, 0x33, 0xd3, 0x9d, 0x4d, 0x34, 0x94, 0x5c, 0xbc, 0x79, 0x11, 0xc1, 0xbb,
+	0x27, 0x3f, 0x82, 0x1f, 0xc1, 0x83, 0x17, 0xb1, 0xe0, 0xc5, 0xa3, 0x24, 0x7e, 0x10, 0xc9, 0xbc,
+	0x24, 0x34, 0xcd, 0x26, 0x69, 0x6f, 0xbb, 0x3b, 0xbf, 0xff, 0x3c, 0xbf, 0x67, 0xf6, 0x61, 0x80,
+	0xdb, 0x22, 0xb2, 0xc3, 0x25, 0x12, 0x31, 0x6f, 0xd3, 0x88, 0x48, 0xd4, 0xab, 0xa3, 0xe3, 0x2e,
+	0x89, 0xfb, 0x9e, 0x88, 0x79, 0xc2, 0x21, 0xd4, 0xeb, 0x9e, 0x5d, 0xf7, 0x7a, 0xf5, 0xdc, 0xcd,
+	0x90, 0x87, 0x5c, 0x2d, 0xa3, 0xf1, 0x93, 0x26, 0x73, 0xf9, 0x90, 0xf3, 0x30, 0x22, 0x08, 0x0b,
+	0x8a, 0x30, 0x63, 0x3c, 0xc1, 0x09, 0xe5, 0x4c, 0x9a, 0xd5, 0xac, 0x59, 0x55, 0x6f, 0x7e, 0xb7,
+	0x8d, 0x30, 0x33, 0x25, 0x72, 0xc5, 0x34, 0x85, 0xa6, 0xf9, 0x62, 0xb8, 0x6a, 0x2a, 0xd7, 0x4a,
+	0x70, 0xd8, 0x8c, 0xc9, 0x71, 0x97, 0xc8, 0xc4, 0x16, 0x2c, 0xa4, 0xef, 0x8a, 0x63, 0xdc, 0xb1,
+	0x58, 0x25, 0x15, 0x0b, 0x8e, 0x30, 0x65, 0xcd, 0x88, 0xb2, 0x37, 0x96, 0x2d, 0xa7, 0xb2, 0x58,
+	0x88, 0x33, 0x64, 0x36, 0xe0, 0x63, 0xb2, 0xa9, 0x0f, 0x49, 0xbf, 0xd8, 0x82, 0xfa, 0x0d, 0xf9,
+	0x58, 0x12, 0x9d, 0x46, 0xbd, 0x9a, 0x4f, 0x12, 0x5c, 0x43, 0x02, 0x87, 0x94, 0xa9, 0x53, 0xd3,
+	0x6c, 0xfd, 0x3b, 0x00, 0x57, 0x5f, 0x8e, 0x11, 0xf8, 0xc1, 0x01, 0xd7, 0x0e, 0x74, 0x59, 0x58,
+	0xf2, 0xce, 0xff, 0x13, 0x4f, 0x61, 0x86, 0x78, 0xa5, 0x4f, 0x22, 0x57, 0x5e, 0x0e, 0x4a, 0xc1,
+	0x99, 0x24, 0x1b, 0x9b, 0xef, 0x7f, 0xff, 0xfb, 0x7c, 0xa5, 0x00, 0xef, 0xa2, 0x39, 0x2d, 0x4e,
+	0x9e, 0x4f, 0xba, 0x92, 0xc4, 0x03, 0xf8, 0xcb, 0x01, 0xf9, 0x7d, 0x16, 0xf0, 0x0e, 0x65, 0xe1,
+	0xde, 0x21, 0x0e, 0x0f, 0x63, 0xcc, 0x64, 0x9b, 0xc4, 0xa6, 0xac, 0x84, 0x8f, 0x53, 0xeb, 0x2e,
+	0x8a, 0x59, 0xeb, 0x27, 0x97, 0x4c, 0x9b, 0x56, 0xea, 0xaa, 0x95, 0x2a, 0xac, 0xcc, 0x6b, 0x45,
+	0x0d, 0x4a, 0x62, 0xa2, 0x93, 0x89, 0x81, 0x1f, 0x1d, 0x00, 0x76, 0xc7, 0xbf, 0xfb, 0xf9, 0xf8,
+	0x1f, 0xc2, 0x4a, 0xaa, 0xc1, 0x14, 0xb2, 0xb6, 0x9b, 0x2b, 0xb1, 0xc6, 0xad, 0xa4, 0xdc, 0xee,
+	0xc0, 0xf5, 0x79, 0x6e, 0x6a, 0xde, 0xb6, 0xd4, 0x14, 0xc1, 0xaf, 0x0e, 0xb8, 0x3e, 0xc9, 0xbf,
+	0x78, 0xcb, 0x48, 0x2c, 0x21, 0x5a, 0x5e, 0x49, 0x93, 0x56, 0xed, 0xfe, 0xea, 0x01, 0xe3, 0xe7,
+	0x29, 0xbf, 0x32, 0x2c, 0x2e, 0xf1, 0x43, 0x5c, 0x2b, 0x7d, 0x71, 0xc0, 0x8d, 0xa7, 0x42, 0x44,
+	0x34, 0x50, 0x53, 0xab, 0x4f, 0x2f, 0xbd, 0xec, 0x2c, 0x6a, 0x45, 0x6b, 0x17, 0x48, 0x18, 0xd3,
+	0x82, 0x32, 0x5d, 0x87, 0xb7, 0xe7, 0x99, 0x62, 0x21, 0xcc, 0x39, 0xfe, 0x74, 0x40, 0x76, 0x66,
+	0x8f, 0x9d, 0xfe, 0x6e, 0x44, 0x09, 0x4b, 0xf6, 0xf7, 0xe0, 0xf6, 0xaa, 0x75, 0xa7, 0x19, 0xab,
+	0xfc, 0xe8, 0x32, 0x51, 0xe3, 0xbe, 0xad, 0xdc, 0x1b, 0xb0, 0xb6, 0xd0, 0x1d, 0x05, 0x2a, 0x27,
+	0xd1, 0x89, 0x7e, 0x68, 0xd2, 0xd6, 0x00, 0x7e, 0x73, 0xc0, 0xad, 0x99, 0x02, 0x66, 0x3a, 0x1e,
+	0xac, 0x2a, 0x74, 0x76, 0x46, 0x1e, 0x5e, 0x34, 0x66, 0x7a, 0xa8, 0xaa, 0x1e, 0x8a, 0xf0, 0xde,
+	0xe2, 0x1e, 0xcc, 0x9c, 0x0c, 0xc0, 0xda, 0x81, 0xba, 0x74, 0x61, 0x31, 0xfd, 0x4a, 0x52, 0x80,
+	0xf5, 0x2a, 0x2d, 0xe5, 0x8c, 0xc8, 0x86, 0x12, 0xc9, 0xc3, 0xdc, 0xdc, 0x9b, 0x4b, 0xb1, 0x3b,
+	0xcf, 0x7e, 0x0c, 0x5d, 0xe7, 0x74, 0xe8, 0x3a, 0x7f, 0x87, 0xae, 0xf3, 0x69, 0xe4, 0x66, 0x4e,
+	0x47, 0x6e, 0xe6, 0xcf, 0xc8, 0xcd, 0xbc, 0xae, 0x85, 0x34, 0x39, 0xea, 0xfa, 0x5e, 0xc0, 0x3b,
+	0x26, 0xbf, 0x15, 0x61, 0x5f, 0xda, 0xbd, 0x7a, 0x0d, 0xf4, 0x6e, 0xba, 0x61, 0xd2, 0x17, 0x44,
+	0xfa, 0x6b, 0xea, 0x6a, 0x6e, 0xfc, 0x0f, 0x00, 0x00, 0xff, 0xff, 0x2e, 0xcf, 0xbd, 0x67, 0x39,
+	0x07, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -92,12 +97,18 @@ type QueryClient interface {
 	// ChainLinks queries the chain links associated to the given user, if
 	// provided. Otherwise it queries all the chain links stored.
 	ChainLinks(ctx context.Context, in *QueryChainLinksRequest, opts ...grpc.CallOption) (*QueryChainLinksResponse, error)
+	// ChainLinkOwners queries for the owners of chain links, optionally searching
+	// for a specific chain name and external address
+	ChainLinkOwners(ctx context.Context, in *QueryChainLinkOwnersRequest, opts ...grpc.CallOption) (*QueryChainLinkOwnersResponse, error)
 	// ApplicationLinks queries the applications links associated to the given
 	// user, if provided. Otherwise, it queries all the application links stored.
 	ApplicationLinks(ctx context.Context, in *QueryApplicationLinksRequest, opts ...grpc.CallOption) (*QueryApplicationLinksResponse, error)
 	// ApplicationLinkByClientID queries a single application link for a given
 	// client id.
 	ApplicationLinkByClientID(ctx context.Context, in *QueryApplicationLinkByClientIDRequest, opts ...grpc.CallOption) (*QueryApplicationLinkByClientIDResponse, error)
+	// ApplicationLinkOwners queries for the owners of applications links,
+	// optionally searching for a specific application and username.
+	ApplicationLinkOwners(ctx context.Context, in *QueryApplicationLinkOwnersRequest, opts ...grpc.CallOption) (*QueryApplicationLinkOwnersResponse, error)
 	// Params queries the profiles module params
 	Params(ctx context.Context, in *QueryParamsRequest, opts ...grpc.CallOption) (*QueryParamsResponse, error)
 }
@@ -137,6 +148,15 @@ func (c *queryClient) ChainLinks(ctx context.Context, in *QueryChainLinksRequest
 	return out, nil
 }
 
+func (c *queryClient) ChainLinkOwners(ctx context.Context, in *QueryChainLinkOwnersRequest, opts ...grpc.CallOption) (*QueryChainLinkOwnersResponse, error) {
+	out := new(QueryChainLinkOwnersResponse)
+	err := c.cc.Invoke(ctx, "/desmos.profiles.v2.Query/ChainLinkOwners", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *queryClient) ApplicationLinks(ctx context.Context, in *QueryApplicationLinksRequest, opts ...grpc.CallOption) (*QueryApplicationLinksResponse, error) {
 	out := new(QueryApplicationLinksResponse)
 	err := c.cc.Invoke(ctx, "/desmos.profiles.v2.Query/ApplicationLinks", in, out, opts...)
@@ -155,6 +175,15 @@ func (c *queryClient) ApplicationLinkByClientID(ctx context.Context, in *QueryAp
 	return out, nil
 }
 
+func (c *queryClient) ApplicationLinkOwners(ctx context.Context, in *QueryApplicationLinkOwnersRequest, opts ...grpc.CallOption) (*QueryApplicationLinkOwnersResponse, error) {
+	out := new(QueryApplicationLinkOwnersResponse)
+	err := c.cc.Invoke(ctx, "/desmos.profiles.v2.Query/ApplicationLinkOwners", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *queryClient) Params(ctx context.Context, in *QueryParamsRequest, opts ...grpc.CallOption) (*QueryParamsResponse, error) {
 	out := new(QueryParamsResponse)
 	err := c.cc.Invoke(ctx, "/desmos.profiles.v2.Query/Params", in, out, opts...)
@@ -176,12 +205,18 @@ type QueryServer interface {
 	// ChainLinks queries the chain links associated to the given user, if
 	// provided. Otherwise it queries all the chain links stored.
 	ChainLinks(context.Context, *QueryChainLinksRequest) (*QueryChainLinksResponse, error)
+	// ChainLinkOwners queries for the owners of chain links, optionally searching
+	// for a specific chain name and external address
+	ChainLinkOwners(context.Context, *QueryChainLinkOwnersRequest) (*QueryChainLinkOwnersResponse, error)
 	// ApplicationLinks queries the applications links associated to the given
 	// user, if provided. Otherwise, it queries all the application links stored.
 	ApplicationLinks(context.Context, *QueryApplicationLinksRequest) (*QueryApplicationLinksResponse, error)
 	// ApplicationLinkByClientID queries a single application link for a given
 	// client id.
 	ApplicationLinkByClientID(context.Context, *QueryApplicationLinkByClientIDRequest) (*QueryApplicationLinkByClientIDResponse, error)
+	// ApplicationLinkOwners queries for the owners of applications links,
+	// optionally searching for a specific application and username.
+	ApplicationLinkOwners(context.Context, *QueryApplicationLinkOwnersRequest) (*QueryApplicationLinkOwnersResponse, error)
 	// Params queries the profiles module params
 	Params(context.Context, *QueryParamsRequest) (*QueryParamsResponse, error)
 }
@@ -199,12 +234,18 @@ func (*UnimplementedQueryServer) IncomingDTagTransferRequests(ctx context.Contex
 func (*UnimplementedQueryServer) ChainLinks(ctx context.Context, req *QueryChainLinksRequest) (*QueryChainLinksResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method ChainLinks not implemented")
 }
+func (*UnimplementedQueryServer) ChainLinkOwners(ctx context.Context, req *QueryChainLinkOwnersRequest) (*QueryChainLinkOwnersResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ChainLinkOwners not implemented")
+}
 func (*UnimplementedQueryServer) ApplicationLinks(ctx context.Context, req *QueryApplicationLinksRequest) (*QueryApplicationLinksResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method ApplicationLinks not implemented")
 }
 func (*UnimplementedQueryServer) ApplicationLinkByClientID(ctx context.Context, req *QueryApplicationLinkByClientIDRequest) (*QueryApplicationLinkByClientIDResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method ApplicationLinkByClientID not implemented")
 }
+func (*UnimplementedQueryServer) ApplicationLinkOwners(ctx context.Context, req *QueryApplicationLinkOwnersRequest) (*QueryApplicationLinkOwnersResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ApplicationLinkOwners not implemented")
+}
 func (*UnimplementedQueryServer) Params(ctx context.Context, req *QueryParamsRequest) (*QueryParamsResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method Params not implemented")
 }
@@ -267,6 +308,24 @@ func _Query_ChainLinks_Handler(srv interface{}, ctx context.Context, dec func(in
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Query_ChainLinkOwners_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(QueryChainLinkOwnersRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(QueryServer).ChainLinkOwners(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/desmos.profiles.v2.Query/ChainLinkOwners",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(QueryServer).ChainLinkOwners(ctx, req.(*QueryChainLinkOwnersRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _Query_ApplicationLinks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(QueryApplicationLinksRequest)
 	if err := dec(in); err != nil {
@@ -303,6 +362,24 @@ func _Query_ApplicationLinkByClientID_Handler(srv interface{}, ctx context.Conte
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Query_ApplicationLinkOwners_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(QueryApplicationLinkOwnersRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(QueryServer).ApplicationLinkOwners(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/desmos.profiles.v2.Query/ApplicationLinkOwners",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(QueryServer).ApplicationLinkOwners(ctx, req.(*QueryApplicationLinkOwnersRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _Query_Params_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(QueryParamsRequest)
 	if err := dec(in); err != nil {
@@ -337,6 +414,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{
 			MethodName: "ChainLinks",
 			Handler:    _Query_ChainLinks_Handler,
 		},
+		{
+			MethodName: "ChainLinkOwners",
+			Handler:    _Query_ChainLinkOwners_Handler,
+		},
 		{
 			MethodName: "ApplicationLinks",
 			Handler:    _Query_ApplicationLinks_Handler,
@@ -345,6 +426,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{
 			MethodName: "ApplicationLinkByClientID",
 			Handler:    _Query_ApplicationLinkByClientID_Handler,
 		},
+		{
+			MethodName: "ApplicationLinkOwners",
+			Handler:    _Query_ApplicationLinkOwners_Handler,
+		},
 		{
 			MethodName: "Params",
 			Handler:    _Query_Params_Handler,
diff --git a/x/profiles/types/query.pb.gw.go b/x/profiles/types/query.pb.gw.go
index 97c664fd..6acc9045 100644
--- a/x/profiles/types/query.pb.gw.go
+++ b/x/profiles/types/query.pb.gw.go
@@ -157,6 +157,42 @@ func local_request_Query_ChainLinks_0(ctx context.Context, marshaler runtime.Mar
 
 }
 
+var (
+	filter_Query_ChainLinkOwners_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
+)
+
+func request_Query_ChainLinkOwners_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryChainLinkOwnersRequest
+	var metadata runtime.ServerMetadata
+
+	if err := req.ParseForm(); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_ChainLinkOwners_0); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.ChainLinkOwners(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Query_ChainLinkOwners_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryChainLinkOwnersRequest
+	var metadata runtime.ServerMetadata
+
+	if err := req.ParseForm(); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_ChainLinkOwners_0); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.ChainLinkOwners(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
 var (
 	filter_Query_ApplicationLinks_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
 )
@@ -247,6 +283,42 @@ func local_request_Query_ApplicationLinkByClientID_0(ctx context.Context, marsha
 
 }
 
+var (
+	filter_Query_ApplicationLinkOwners_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
+)
+
+func request_Query_ApplicationLinkOwners_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryApplicationLinkOwnersRequest
+	var metadata runtime.ServerMetadata
+
+	if err := req.ParseForm(); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_ApplicationLinkOwners_0); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.ApplicationLinkOwners(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Query_ApplicationLinkOwners_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq QueryApplicationLinkOwnersRequest
+	var metadata runtime.ServerMetadata
+
+	if err := req.ParseForm(); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_ApplicationLinkOwners_0); err != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.ApplicationLinkOwners(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
 func request_Query_Params_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
 	var protoReq QueryParamsRequest
 	var metadata runtime.ServerMetadata
@@ -331,6 +403,26 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv
 
 	})
 
+	mux.Handle("GET", pattern_Query_ChainLinkOwners_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_ChainLinkOwners_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_ChainLinkOwners_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	mux.Handle("GET", pattern_Query_ApplicationLinks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
@@ -371,6 +463,26 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv
 
 	})
 
+	mux.Handle("GET", pattern_Query_ApplicationLinkOwners_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_ApplicationLinkOwners_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_ApplicationLinkOwners_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	mux.Handle("GET", pattern_Query_Params_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
@@ -492,6 +604,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie
 
 	})
 
+	mux.Handle("GET", pattern_Query_ChainLinkOwners_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_ChainLinkOwners_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_ChainLinkOwners_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	mux.Handle("GET", pattern_Query_ApplicationLinks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
@@ -532,6 +664,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie
 
 	})
 
+	mux.Handle("GET", pattern_Query_ApplicationLinkOwners_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_ApplicationLinkOwners_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_ApplicationLinkOwners_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	mux.Handle("GET", pattern_Query_Params_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
@@ -562,10 +714,14 @@ var (
 
 	pattern_Query_ChainLinks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"desmos", "profiles", "v2", "chain-links"}, "", runtime.AssumeColonVerbOpt(true)))
 
+	pattern_Query_ChainLinkOwners_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"desmos", "profiles", "v2", "chain-links", "owners"}, "", runtime.AssumeColonVerbOpt(true)))
+
 	pattern_Query_ApplicationLinks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"desmos", "profiles", "v2", "app-links"}, "", 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", "v2", "app-links", "clients", "client_id"}, "", runtime.AssumeColonVerbOpt(true)))
 
+	pattern_Query_ApplicationLinkOwners_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"desmos", "profiles", "v2", "app-links", "owners"}, "", runtime.AssumeColonVerbOpt(true)))
+
 	pattern_Query_Params_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"desmos", "profiles", "v2", "params"}, "", runtime.AssumeColonVerbOpt(true)))
 )
 
@@ -576,9 +732,13 @@ var (
 
 	forward_Query_ChainLinks_0 = runtime.ForwardResponseMessage
 
+	forward_Query_ChainLinkOwners_0 = runtime.ForwardResponseMessage
+
 	forward_Query_ApplicationLinks_0 = runtime.ForwardResponseMessage
 
 	forward_Query_ApplicationLinkByClientID_0 = runtime.ForwardResponseMessage
 
+	forward_Query_ApplicationLinkOwners_0 = runtime.ForwardResponseMessage
+
 	forward_Query_Params_0 = runtime.ForwardResponseMessage
 )
diff --git a/x/profiles/types/query_app_links.go b/x/profiles/types/query_app_links.go
index 14e0865e..bec8d101 100644
--- a/x/profiles/types/query_app_links.go
+++ b/x/profiles/types/query_app_links.go
@@ -24,3 +24,14 @@ func NewQueryApplicationLinkByClientIDRequest(clientID string) *QueryApplication
 		ClientId: clientID,
 	}
 }
+
+// NewQueryApplicationLinkOwnersRequest returns a new QueryApplicationLinkOwnersRequest instance
+func NewQueryApplicationLinkOwnersRequest(
+	application, username string, pageReq *query.PageRequest,
+) *QueryApplicationLinkOwnersRequest {
+	return &QueryApplicationLinkOwnersRequest{
+		Application: application,
+		Username:    username,
+		Pagination:  pageReq,
+	}
+}
diff --git a/x/profiles/types/query_app_links.pb.go b/x/profiles/types/query_app_links.pb.go
index 12c433a0..32641d7b 100644
--- a/x/profiles/types/query_app_links.pb.go
+++ b/x/profiles/types/query_app_links.pb.go
@@ -254,11 +254,203 @@ func (m *QueryApplicationLinkByClientIDResponse) GetLink() ApplicationLink {
 	return ApplicationLink{}
 }
 
+// QueryApplicationLinkOwnersRequest contains the data of the request that can
+// be used to get application link owners
+type QueryApplicationLinkOwnersRequest struct {
+	// (Optional) Application name to search link owners of. If not specified, all
+	// links stored will be searched instead.
+	Application string `protobuf:"bytes,1,opt,name=application,proto3" json:"application,omitempty"`
+	// (Optional) Username to search for. This will only be used if the
+	// application is specified as well
+	Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
+	// Pagination defines an optional pagination for the request
+	Pagination *query.PageRequest `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"`
+}
+
+func (m *QueryApplicationLinkOwnersRequest) Reset()         { *m = QueryApplicationLinkOwnersRequest{} }
+func (m *QueryApplicationLinkOwnersRequest) String() string { return proto.CompactTextString(m) }
+func (*QueryApplicationLinkOwnersRequest) ProtoMessage()    {}
+func (*QueryApplicationLinkOwnersRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_df55d222350c3dbf, []int{4}
+}
+func (m *QueryApplicationLinkOwnersRequest) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryApplicationLinkOwnersRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryApplicationLinkOwnersRequest.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 *QueryApplicationLinkOwnersRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryApplicationLinkOwnersRequest.Merge(m, src)
+}
+func (m *QueryApplicationLinkOwnersRequest) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryApplicationLinkOwnersRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryApplicationLinkOwnersRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryApplicationLinkOwnersRequest proto.InternalMessageInfo
+
+func (m *QueryApplicationLinkOwnersRequest) GetApplication() string {
+	if m != nil {
+		return m.Application
+	}
+	return ""
+}
+
+func (m *QueryApplicationLinkOwnersRequest) GetUsername() string {
+	if m != nil {
+		return m.Username
+	}
+	return ""
+}
+
+func (m *QueryApplicationLinkOwnersRequest) GetPagination() *query.PageRequest {
+	if m != nil {
+		return m.Pagination
+	}
+	return nil
+}
+
+// QueryApplicationLinkOwnersResponse contains the data returned by the request
+// allowing to get application link owners.
+type QueryApplicationLinkOwnersResponse struct {
+	// Addresses of the application links owners
+	Owners []QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails `protobuf:"bytes,1,rep,name=owners,proto3" json:"owners"`
+	// Pagination defines the pagination response
+	Pagination *query.PageResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"`
+}
+
+func (m *QueryApplicationLinkOwnersResponse) Reset()         { *m = QueryApplicationLinkOwnersResponse{} }
+func (m *QueryApplicationLinkOwnersResponse) String() string { return proto.CompactTextString(m) }
+func (*QueryApplicationLinkOwnersResponse) ProtoMessage()    {}
+func (*QueryApplicationLinkOwnersResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_df55d222350c3dbf, []int{5}
+}
+func (m *QueryApplicationLinkOwnersResponse) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryApplicationLinkOwnersResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryApplicationLinkOwnersResponse.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 *QueryApplicationLinkOwnersResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryApplicationLinkOwnersResponse.Merge(m, src)
+}
+func (m *QueryApplicationLinkOwnersResponse) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryApplicationLinkOwnersResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryApplicationLinkOwnersResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryApplicationLinkOwnersResponse proto.InternalMessageInfo
+
+func (m *QueryApplicationLinkOwnersResponse) GetOwners() []QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails {
+	if m != nil {
+		return m.Owners
+	}
+	return nil
+}
+
+func (m *QueryApplicationLinkOwnersResponse) GetPagination() *query.PageResponse {
+	if m != nil {
+		return m.Pagination
+	}
+	return nil
+}
+
+// ApplicationLinkOwnerDetails contains the details of a single application
+// link owner
+type QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails struct {
+	User        string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+	Application string `protobuf:"bytes,2,opt,name=application,proto3" json:"application,omitempty"`
+	Username    string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
+}
+
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) Reset() {
+	*m = QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails{}
+}
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) String() string {
+	return proto.CompactTextString(m)
+}
+func (*QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) ProtoMessage() {}
+func (*QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) Descriptor() ([]byte, []int) {
+	return fileDescriptor_df55d222350c3dbf, []int{5, 0}
+}
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails.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 *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails.Merge(m, src)
+}
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails proto.InternalMessageInfo
+
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) GetUser() string {
+	if m != nil {
+		return m.User
+	}
+	return ""
+}
+
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) GetApplication() string {
+	if m != nil {
+		return m.Application
+	}
+	return ""
+}
+
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) GetUsername() string {
+	if m != nil {
+		return m.Username
+	}
+	return ""
+}
+
 func init() {
 	proto.RegisterType((*QueryApplicationLinksRequest)(nil), "desmos.profiles.v2.QueryApplicationLinksRequest")
 	proto.RegisterType((*QueryApplicationLinksResponse)(nil), "desmos.profiles.v2.QueryApplicationLinksResponse")
 	proto.RegisterType((*QueryApplicationLinkByClientIDRequest)(nil), "desmos.profiles.v2.QueryApplicationLinkByClientIDRequest")
 	proto.RegisterType((*QueryApplicationLinkByClientIDResponse)(nil), "desmos.profiles.v2.QueryApplicationLinkByClientIDResponse")
+	proto.RegisterType((*QueryApplicationLinkOwnersRequest)(nil), "desmos.profiles.v2.QueryApplicationLinkOwnersRequest")
+	proto.RegisterType((*QueryApplicationLinkOwnersResponse)(nil), "desmos.profiles.v2.QueryApplicationLinkOwnersResponse")
+	proto.RegisterType((*QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails)(nil), "desmos.profiles.v2.QueryApplicationLinkOwnersResponse.ApplicationLinkOwnerDetails")
 }
 
 func init() {
@@ -266,36 +458,40 @@ func init() {
 }
 
 var fileDescriptor_df55d222350c3dbf = []byte{
-	// 450 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x92, 0x41, 0x6f, 0xd3, 0x30,
-	0x14, 0xc7, 0xeb, 0xad, 0xa0, 0xcd, 0xbd, 0x59, 0x1c, 0xb2, 0x30, 0x42, 0x55, 0xc4, 0x28, 0x48,
-	0xd8, 0x6a, 0x77, 0x46, 0x88, 0x32, 0x81, 0x26, 0x38, 0x40, 0x8f, 0x5c, 0x2a, 0x27, 0xf1, 0x8c,
-	0x45, 0x62, 0x7b, 0xb1, 0x13, 0x91, 0x6f, 0xc1, 0xd7, 0xe0, 0x1b, 0xf0, 0x11, 0x76, 0xdc, 0x91,
-	0x13, 0x42, 0xed, 0x17, 0x41, 0xb1, 0x3d, 0xba, 0x8d, 0xa1, 0x6a, 0x37, 0xbf, 0xf7, 0xff, 0xbf,
-	0xf7, 0x7e, 0xef, 0x25, 0x70, 0x9c, 0x33, 0x53, 0x2a, 0x43, 0x74, 0xa5, 0x4e, 0x44, 0xc1, 0x0c,
-	0x69, 0xa6, 0xe4, 0xb4, 0x66, 0x55, 0xbb, 0xa0, 0x5a, 0x2f, 0x0a, 0x21, 0xbf, 0x18, 0xac, 0x2b,
-	0x65, 0x15, 0x42, 0xde, 0x89, 0x2f, 0x9c, 0xb8, 0x99, 0xc6, 0xf7, 0xb8, 0xe2, 0xca, 0xc9, 0xa4,
-	0x7b, 0x79, 0x67, 0xbc, 0xcf, 0x95, 0xe2, 0x05, 0x23, 0x54, 0x0b, 0x42, 0xa5, 0x54, 0x96, 0x5a,
-	0xa1, 0x64, 0xe8, 0x13, 0xef, 0x05, 0xd5, 0x45, 0x69, 0x7d, 0x42, 0xa8, 0x6c, 0x83, 0xf4, 0xf4,
-	0x06, 0x98, 0x52, 0xe5, 0xac, 0x30, 0xd7, 0x69, 0xe2, 0xbd, 0x4c, 0x75, 0xd6, 0x85, 0x1f, 0xee,
-	0x83, 0x20, 0x3d, 0xf3, 0x11, 0x49, 0xa9, 0x61, 0x7e, 0x17, 0xd2, 0x4c, 0x52, 0x66, 0xe9, 0x84,
-	0x68, 0xca, 0x85, 0x74, 0x34, 0xde, 0x3b, 0xfa, 0x01, 0xe0, 0xfe, 0xc7, 0xce, 0xf2, 0x4a, 0xeb,
-	0x42, 0x64, 0x4e, 0x7a, 0xdf, 0x8d, 0x99, 0xb3, 0xd3, 0x9a, 0x19, 0x8b, 0x10, 0xec, 0xd7, 0x86,
-	0x55, 0x11, 0x18, 0x82, 0xf1, 0xee, 0xdc, 0xbd, 0xd1, 0x10, 0x0e, 0xe8, 0xda, 0x1e, 0x6d, 0x39,
-	0xe9, 0x72, 0x0a, 0xc5, 0x70, 0xa7, 0x73, 0x4a, 0x5a, 0xb2, 0x68, 0xdb, 0xc9, 0x7f, 0x63, 0xf4,
-	0x06, 0xc2, 0x35, 0x46, 0xd4, 0x1f, 0x82, 0xf1, 0x60, 0x7a, 0x80, 0xc3, 0x06, 0x1d, 0x33, 0x76,
-	0xcc, 0x38, 0x30, 0xe3, 0x0f, 0x94, 0xb3, 0x40, 0x33, 0xbf, 0x54, 0x39, 0xfa, 0x0e, 0xe0, 0x83,
-	0xff, 0xa0, 0x1b, 0xad, 0xa4, 0x61, 0xe8, 0x25, 0xbc, 0xe3, 0x4e, 0x16, 0x81, 0xe1, 0xf6, 0x78,
-	0x30, 0x7d, 0x84, 0xff, 0xfd, 0x82, 0xf8, 0x5a, 0xf1, 0xac, 0x7f, 0xf6, 0xeb, 0x61, 0x6f, 0xee,
-	0xeb, 0xd0, 0xdb, 0x2b, 0xa8, 0x5b, 0x0e, 0xf5, 0xc9, 0x46, 0x54, 0x3f, 0xfd, 0x0a, 0xeb, 0x11,
-	0x7c, 0x7c, 0x13, 0xea, 0xac, 0x7d, 0x5d, 0x08, 0x26, 0xed, 0xf1, 0xd1, 0xc5, 0xb9, 0xef, 0xc3,
-	0xdd, 0xcc, 0xa5, 0x16, 0x22, 0x0f, 0x37, 0xdf, 0xf1, 0x89, 0xe3, 0x7c, 0xc4, 0xe1, 0xc1, 0xa6,
-	0x2e, 0x61, 0xf3, 0x17, 0xb0, 0xdf, 0x6d, 0xe0, 0x3a, 0xdc, 0x6a, 0x71, 0x57, 0x36, 0x7b, 0x77,
-	0xb6, 0x4c, 0xc0, 0xf9, 0x32, 0x01, 0xbf, 0x97, 0x09, 0xf8, 0xb6, 0x4a, 0x7a, 0xe7, 0xab, 0xa4,
-	0xf7, 0x73, 0x95, 0xf4, 0x3e, 0x4d, 0xb8, 0xb0, 0x9f, 0xeb, 0x14, 0x67, 0xaa, 0x24, 0xbe, 0xe9,
-	0xf3, 0x82, 0xa6, 0x26, 0xbc, 0x49, 0x73, 0x48, 0xbe, 0xae, 0xff, 0x5e, 0xdb, 0x6a, 0x66, 0xd2,
-	0xbb, 0xee, 0x4f, 0x3b, 0xfc, 0x13, 0x00, 0x00, 0xff, 0xff, 0x70, 0x72, 0x08, 0x47, 0x6a, 0x03,
-	0x00, 0x00,
+	// 521 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x54, 0xc1, 0x6e, 0x13, 0x31,
+	0x10, 0x8d, 0x93, 0x50, 0xb5, 0xce, 0xcd, 0xe2, 0x90, 0x6e, 0xcb, 0x12, 0x82, 0x28, 0x01, 0x09,
+	0x5b, 0x49, 0xcf, 0x08, 0x11, 0x22, 0x50, 0x05, 0x12, 0xb0, 0x47, 0x2e, 0x91, 0x37, 0x71, 0x17,
+	0x8b, 0x8d, 0xed, 0xae, 0xbd, 0x81, 0xfc, 0x05, 0xbf, 0xc1, 0x1f, 0xf0, 0x09, 0x3d, 0xe6, 0xc8,
+	0x09, 0xa1, 0xe4, 0x47, 0xd0, 0xda, 0x6e, 0xd3, 0x86, 0x25, 0x11, 0xa8, 0x37, 0xcf, 0xbc, 0x37,
+	0x33, 0x6f, 0x5e, 0x26, 0x0b, 0x3b, 0x63, 0xa6, 0x27, 0x52, 0x13, 0x95, 0xc9, 0x53, 0x9e, 0x32,
+	0x4d, 0xa6, 0x3d, 0x72, 0x96, 0xb3, 0x6c, 0x36, 0xa4, 0x4a, 0x0d, 0x53, 0x2e, 0x3e, 0x69, 0xac,
+	0x32, 0x69, 0x24, 0x42, 0x8e, 0x89, 0x2f, 0x98, 0x78, 0xda, 0x0b, 0x6e, 0x27, 0x32, 0x91, 0x16,
+	0x26, 0xc5, 0xcb, 0x31, 0x83, 0xc3, 0x44, 0xca, 0x24, 0x65, 0x84, 0x2a, 0x4e, 0xa8, 0x10, 0xd2,
+	0x50, 0xc3, 0xa5, 0xf0, 0x7d, 0x82, 0x7d, 0x8f, 0xda, 0x28, 0xce, 0x4f, 0x09, 0x15, 0x33, 0x0f,
+	0x3d, 0x2a, 0x11, 0x33, 0x91, 0x63, 0x96, 0xea, 0x75, 0x35, 0xc1, 0xfe, 0x48, 0x16, 0xd4, 0xa1,
+	0x1b, 0xee, 0x02, 0x0f, 0x3d, 0x76, 0x11, 0x89, 0xa9, 0x66, 0x6e, 0x17, 0x32, 0xed, 0xc6, 0xcc,
+	0xd0, 0x2e, 0x51, 0x34, 0xe1, 0xc2, 0xaa, 0x71, 0xdc, 0xf6, 0x77, 0x00, 0x0f, 0xdf, 0x17, 0x94,
+	0xe7, 0x4a, 0xa5, 0x7c, 0x64, 0xa1, 0x37, 0xc5, 0x98, 0x88, 0x9d, 0xe5, 0x4c, 0x1b, 0x84, 0x60,
+	0x3d, 0xd7, 0x2c, 0x6b, 0x82, 0x16, 0xe8, 0xec, 0x45, 0xf6, 0x8d, 0x5a, 0xb0, 0x41, 0x57, 0xf4,
+	0x66, 0xd5, 0x42, 0x57, 0x53, 0x28, 0x80, 0xbb, 0x05, 0x53, 0xd0, 0x09, 0x6b, 0xd6, 0x2c, 0x7c,
+	0x19, 0xa3, 0x97, 0x10, 0xae, 0x64, 0x34, 0xeb, 0x2d, 0xd0, 0x69, 0xf4, 0x8e, 0xb0, 0xdf, 0xa0,
+	0xd0, 0x8c, 0xad, 0x66, 0xec, 0x35, 0xe3, 0x77, 0x34, 0x61, 0x5e, 0x4d, 0x74, 0xa5, 0xb2, 0xfd,
+	0x0d, 0xc0, 0x3b, 0x7f, 0x91, 0xae, 0x95, 0x14, 0x9a, 0xa1, 0x67, 0xf0, 0x96, 0xb5, 0xac, 0x09,
+	0x5a, 0xb5, 0x4e, 0xa3, 0x77, 0x1f, 0xff, 0xf9, 0x0b, 0xe2, 0xb5, 0xe2, 0x7e, 0xfd, 0xfc, 0xe7,
+	0xdd, 0x4a, 0xe4, 0xea, 0xd0, 0xab, 0x6b, 0x52, 0xab, 0x56, 0xea, 0xc3, 0xad, 0x52, 0xdd, 0xf4,
+	0x6b, 0x5a, 0x07, 0xf0, 0x41, 0x99, 0xd4, 0xfe, 0xec, 0x45, 0xca, 0x99, 0x30, 0x27, 0x83, 0x0b,
+	0xbb, 0x0f, 0xe0, 0xde, 0xc8, 0xa6, 0x86, 0x7c, 0xec, 0x3d, 0xdf, 0x75, 0x89, 0x93, 0x71, 0x3b,
+	0x81, 0x47, 0xdb, 0xba, 0xf8, 0xcd, 0x9f, 0xc2, 0x7a, 0xb1, 0x81, 0xed, 0xf0, 0x4f, 0x8b, 0xdb,
+	0xb2, 0xc2, 0xda, 0x7b, 0x65, 0x93, 0xde, 0x7e, 0x16, 0x2c, 0xbb, 0x3c, 0x8d, 0xb5, 0x33, 0x00,
+	0x9b, 0xcf, 0xa0, 0xba, 0xf1, 0x0c, 0x6a, 0xff, 0x7d, 0x06, 0xf3, 0x2a, 0x6c, 0x6f, 0xd2, 0xea,
+	0x1d, 0x51, 0x70, 0x47, 0xda, 0x8c, 0x3f, 0x86, 0xa8, 0xcc, 0x93, 0xed, 0x7d, 0x70, 0x19, 0x3a,
+	0x60, 0x86, 0xf2, 0x54, 0x7b, 0x0b, 0xfd, 0x9c, 0x1b, 0x3b, 0x9e, 0x40, 0xc2, 0x83, 0x0d, 0x53,
+	0x6f, 0xfe, 0x1f, 0xda, 0x7f, 0x7d, 0xbe, 0x08, 0xc1, 0x7c, 0x11, 0x82, 0x5f, 0x8b, 0x10, 0x7c,
+	0x5d, 0x86, 0x95, 0xf9, 0x32, 0xac, 0xfc, 0x58, 0x86, 0x95, 0x0f, 0xdd, 0x84, 0x9b, 0x8f, 0x79,
+	0x8c, 0x47, 0x72, 0x42, 0x9c, 0x7f, 0x4f, 0x52, 0x1a, 0x6b, 0xff, 0x26, 0xd3, 0x63, 0xf2, 0x65,
+	0xf5, 0xf1, 0x32, 0x33, 0xc5, 0x74, 0xbc, 0x63, 0x3f, 0x34, 0xc7, 0xbf, 0x03, 0x00, 0x00, 0xff,
+	0xff, 0x13, 0x54, 0x76, 0x6c, 0x69, 0x05, 0x00, 0x00,
 }
 
 func (m *QueryApplicationLinksRequest) Marshal() (dAtA []byte, err error) {
@@ -466,6 +662,148 @@ func (m *QueryApplicationLinkByClientIDResponse) MarshalToSizedBuffer(dAtA []byt
 	return len(dAtA) - i, nil
 }
 
+func (m *QueryApplicationLinkOwnersRequest) 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 *QueryApplicationLinkOwnersRequest) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryApplicationLinkOwnersRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.Pagination != nil {
+		{
+			size, err := m.Pagination.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintQueryAppLinks(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x1a
+	}
+	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] = 0x12
+	}
+	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] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *QueryApplicationLinkOwnersResponse) 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 *QueryApplicationLinkOwnersResponse) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryApplicationLinkOwnersResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.Pagination != nil {
+		{
+			size, err := m.Pagination.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintQueryAppLinks(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x12
+	}
+	if len(m.Owners) > 0 {
+		for iNdEx := len(m.Owners) - 1; iNdEx >= 0; iNdEx-- {
+			{
+				size, err := m.Owners[iNdEx].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 *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) 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 *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) 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 encodeVarintQueryAppLinks(dAtA []byte, offset int, v uint64) int {
 	offset -= sovQueryAppLinks(v)
 	base := offset
@@ -545,13 +883,376 @@ func (m *QueryApplicationLinkByClientIDResponse) 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 *QueryApplicationLinkOwnersRequest) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = 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))
+	}
+	if m.Pagination != nil {
+		l = m.Pagination.Size()
+		n += 1 + l + sovQueryAppLinks(uint64(l))
+	}
+	return n
+}
+
+func (m *QueryApplicationLinkOwnersResponse) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if len(m.Owners) > 0 {
+		for _, e := range m.Owners {
+			l = e.Size()
+			n += 1 + l + sovQueryAppLinks(uint64(l))
+		}
+	}
+	if m.Pagination != nil {
+		l = m.Pagination.Size()
+		n += 1 + l + sovQueryAppLinks(uint64(l))
+	}
+	return n
+}
+
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) 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 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 *QueryApplicationLinksRequest) 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: QueryApplicationLinksRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryApplicationLinksRequest: 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
+		case 4:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Pagination", 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 m.Pagination == nil {
+				m.Pagination = &query.PageRequest{}
+			}
+			if err := m.Pagination.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 *QueryApplicationLinksResponse) 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: QueryApplicationLinksResponse: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryApplicationLinksResponse: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Links", 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
+			}
+			m.Links = append(m.Links, ApplicationLink{})
+			if err := m.Links[len(m.Links)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Pagination", 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 m.Pagination == nil {
+				m.Pagination = &query.PageResponse{}
+			}
+			if err := m.Pagination.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 *QueryApplicationLinksRequest) Unmarshal(dAtA []byte) error {
+func (m *QueryApplicationLinkByClientIDRequest) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -574,15 +1275,15 @@ func (m *QueryApplicationLinksRequest) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: QueryApplicationLinksRequest: wiretype end group for non-group")
+			return fmt.Errorf("proto: QueryApplicationLinkByClientIDRequest: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: QueryApplicationLinksRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+			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 User", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field ClientId", wireType)
 			}
 			var stringLen uint64
 			for shift := uint(0); ; shift += 7 {
@@ -610,9 +1311,142 @@ func (m *QueryApplicationLinksRequest) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.User = string(dAtA[iNdEx:postIndex])
+			m.ClientId = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
-		case 2:
+		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 (m *QueryApplicationLinkOwnersRequest) 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: QueryApplicationLinkOwnersRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryApplicationLinkOwnersRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Application", wireType)
 			}
@@ -644,7 +1478,7 @@ func (m *QueryApplicationLinksRequest) Unmarshal(dAtA []byte) error {
 			}
 			m.Application = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
-		case 3:
+		case 2:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Username", wireType)
 			}
@@ -676,7 +1510,7 @@ func (m *QueryApplicationLinksRequest) Unmarshal(dAtA []byte) error {
 			}
 			m.Username = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
-		case 4:
+		case 3:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Pagination", wireType)
 			}
@@ -733,7 +1567,7 @@ func (m *QueryApplicationLinksRequest) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *QueryApplicationLinksResponse) Unmarshal(dAtA []byte) error {
+func (m *QueryApplicationLinkOwnersResponse) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -756,15 +1590,15 @@ func (m *QueryApplicationLinksResponse) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: QueryApplicationLinksResponse: wiretype end group for non-group")
+			return fmt.Errorf("proto: QueryApplicationLinkOwnersResponse: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: QueryApplicationLinksResponse: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: QueryApplicationLinkOwnersResponse: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
 		case 1:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Links", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field Owners", wireType)
 			}
 			var msglen int
 			for shift := uint(0); ; shift += 7 {
@@ -791,8 +1625,8 @@ func (m *QueryApplicationLinksResponse) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Links = append(m.Links, ApplicationLink{})
-			if err := m.Links[len(m.Links)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+			m.Owners = append(m.Owners, QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails{})
+			if err := m.Owners[len(m.Owners)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
 			iNdEx = postIndex
@@ -853,7 +1687,7 @@ func (m *QueryApplicationLinksResponse) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *QueryApplicationLinkByClientIDRequest) Unmarshal(dAtA []byte) error {
+func (m *QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -876,15 +1710,15 @@ func (m *QueryApplicationLinkByClientIDRequest) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: QueryApplicationLinkByClientIDRequest: wiretype end group for non-group")
+			return fmt.Errorf("proto: ApplicationLinkOwnerDetails: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: QueryApplicationLinkByClientIDRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: ApplicationLinkOwnerDetails: 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)
+				return fmt.Errorf("proto: wrong wireType = %d for field User", wireType)
 			}
 			var stringLen uint64
 			for shift := uint(0); ; shift += 7 {
@@ -912,63 +1746,45 @@ func (m *QueryApplicationLinkByClientIDRequest) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.ClientId = string(dAtA[iNdEx:postIndex])
+			m.User = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
-		default:
-			iNdEx = preIndex
-			skippy, err := skipQueryAppLinks(dAtA[iNdEx:])
-			if err != nil {
-				return err
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Application", wireType)
 			}
-			if (skippy < 0) || (iNdEx+skippy) < 0 {
-				return ErrInvalidLengthQueryAppLinks
+			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
+				}
 			}
-			if (iNdEx + skippy) > l {
-				return io.ErrUnexpectedEOF
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthQueryAppLinks
 			}
-			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
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthQueryAppLinks
 			}
-			if iNdEx >= l {
+			if postIndex > 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:
+			m.Application = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 3:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Link", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field Username", wireType)
 			}
-			var msglen int
+			var stringLen uint64
 			for shift := uint(0); ; shift += 7 {
 				if shift >= 64 {
 					return ErrIntOverflowQueryAppLinks
@@ -978,24 +1794,23 @@ func (m *QueryApplicationLinkByClientIDResponse) Unmarshal(dAtA []byte) error {
 				}
 				b := dAtA[iNdEx]
 				iNdEx++
-				msglen |= int(b&0x7F) << shift
+				stringLen |= uint64(b&0x7F) << shift
 				if b < 0x80 {
 					break
 				}
 			}
-			if msglen < 0 {
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
 				return ErrInvalidLengthQueryAppLinks
 			}
-			postIndex := iNdEx + msglen
+			postIndex := iNdEx + intStringLen
 			if postIndex < 0 {
 				return ErrInvalidLengthQueryAppLinks
 			}
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			if err := m.Link.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
-				return err
-			}
+			m.Username = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
diff --git a/x/profiles/types/query_chain_links.go b/x/profiles/types/query_chain_links.go
index 67353d83..253a69c0 100644
--- a/x/profiles/types/query_chain_links.go
+++ b/x/profiles/types/query_chain_links.go
@@ -17,3 +17,12 @@ func NewQueryChainLinksRequest(
 		Pagination: pageReq,
 	}
 }
+
+// NewQueryChainLinkOwnersRequest returns a new QueryChainLinkOwnersRequest instance
+func NewQueryChainLinkOwnersRequest(chainName, target string, pageReq *query.PageRequest) *QueryChainLinkOwnersRequest {
+	return &QueryChainLinkOwnersRequest{
+		ChainName:  chainName,
+		Target:     target,
+		Pagination: pageReq,
+	}
+}
diff --git a/x/profiles/types/query_chain_links.pb.go b/x/profiles/types/query_chain_links.pb.go
index 0defe500..57685aee 100644
--- a/x/profiles/types/query_chain_links.pb.go
+++ b/x/profiles/types/query_chain_links.pb.go
@@ -160,9 +160,200 @@ func (m *QueryChainLinksResponse) GetPagination() *query.PageResponse {
 	return nil
 }
 
+// QueryChainLinkOwnersRequest contains the data of the request that can
+// be used to get chain link owners
+type QueryChainLinkOwnersRequest struct {
+	// (Optional) Chain name to search link owners of. If not specified, all
+	// links stored will be searched instead.
+	ChainName string `protobuf:"bytes,1,opt,name=chain_name,json=chainName,proto3" json:"chain_name,omitempty"`
+	// (Optional) External address to search for. This will only be used if the
+	// chain name is specified as well
+	Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"`
+	// Pagination defines an optional pagination for the request
+	Pagination *query.PageRequest `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"`
+}
+
+func (m *QueryChainLinkOwnersRequest) Reset()         { *m = QueryChainLinkOwnersRequest{} }
+func (m *QueryChainLinkOwnersRequest) String() string { return proto.CompactTextString(m) }
+func (*QueryChainLinkOwnersRequest) ProtoMessage()    {}
+func (*QueryChainLinkOwnersRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f3f86dba4268625b, []int{2}
+}
+func (m *QueryChainLinkOwnersRequest) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryChainLinkOwnersRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryChainLinkOwnersRequest.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 *QueryChainLinkOwnersRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryChainLinkOwnersRequest.Merge(m, src)
+}
+func (m *QueryChainLinkOwnersRequest) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryChainLinkOwnersRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryChainLinkOwnersRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryChainLinkOwnersRequest proto.InternalMessageInfo
+
+func (m *QueryChainLinkOwnersRequest) GetChainName() string {
+	if m != nil {
+		return m.ChainName
+	}
+	return ""
+}
+
+func (m *QueryChainLinkOwnersRequest) GetTarget() string {
+	if m != nil {
+		return m.Target
+	}
+	return ""
+}
+
+func (m *QueryChainLinkOwnersRequest) GetPagination() *query.PageRequest {
+	if m != nil {
+		return m.Pagination
+	}
+	return nil
+}
+
+// QueryChainLinkOwnersResponse contains the data returned by the request
+// allowing to get chain link owners.
+type QueryChainLinkOwnersResponse struct {
+	// Addresses of the chain links owners
+	Owners []QueryChainLinkOwnersResponse_ChainLinkOwnerDetails `protobuf:"bytes,1,rep,name=owners,proto3" json:"owners"`
+	// Pagination defines the pagination response
+	Pagination *query.PageResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"`
+}
+
+func (m *QueryChainLinkOwnersResponse) Reset()         { *m = QueryChainLinkOwnersResponse{} }
+func (m *QueryChainLinkOwnersResponse) String() string { return proto.CompactTextString(m) }
+func (*QueryChainLinkOwnersResponse) ProtoMessage()    {}
+func (*QueryChainLinkOwnersResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f3f86dba4268625b, []int{3}
+}
+func (m *QueryChainLinkOwnersResponse) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryChainLinkOwnersResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryChainLinkOwnersResponse.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 *QueryChainLinkOwnersResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryChainLinkOwnersResponse.Merge(m, src)
+}
+func (m *QueryChainLinkOwnersResponse) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryChainLinkOwnersResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryChainLinkOwnersResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryChainLinkOwnersResponse proto.InternalMessageInfo
+
+func (m *QueryChainLinkOwnersResponse) GetOwners() []QueryChainLinkOwnersResponse_ChainLinkOwnerDetails {
+	if m != nil {
+		return m.Owners
+	}
+	return nil
+}
+
+func (m *QueryChainLinkOwnersResponse) GetPagination() *query.PageResponse {
+	if m != nil {
+		return m.Pagination
+	}
+	return nil
+}
+
+// ChainLinkOwnerDetails contains the details of a single chain link owner
+type QueryChainLinkOwnersResponse_ChainLinkOwnerDetails struct {
+	User      string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
+	ChainName string `protobuf:"bytes,2,opt,name=chain_name,json=chainName,proto3" json:"chain_name,omitempty"`
+	Target    string `protobuf:"bytes,3,opt,name=target,proto3" json:"target,omitempty"`
+}
+
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) Reset() {
+	*m = QueryChainLinkOwnersResponse_ChainLinkOwnerDetails{}
+}
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) String() string {
+	return proto.CompactTextString(m)
+}
+func (*QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) ProtoMessage() {}
+func (*QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f3f86dba4268625b, []int{3, 0}
+}
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_QueryChainLinkOwnersResponse_ChainLinkOwnerDetails.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 *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_QueryChainLinkOwnersResponse_ChainLinkOwnerDetails.Merge(m, src)
+}
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) XXX_Size() int {
+	return m.Size()
+}
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) XXX_DiscardUnknown() {
+	xxx_messageInfo_QueryChainLinkOwnersResponse_ChainLinkOwnerDetails.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryChainLinkOwnersResponse_ChainLinkOwnerDetails proto.InternalMessageInfo
+
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) GetUser() string {
+	if m != nil {
+		return m.User
+	}
+	return ""
+}
+
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) GetChainName() string {
+	if m != nil {
+		return m.ChainName
+	}
+	return ""
+}
+
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) GetTarget() string {
+	if m != nil {
+		return m.Target
+	}
+	return ""
+}
+
 func init() {
 	proto.RegisterType((*QueryChainLinksRequest)(nil), "desmos.profiles.v2.QueryChainLinksRequest")
 	proto.RegisterType((*QueryChainLinksResponse)(nil), "desmos.profiles.v2.QueryChainLinksResponse")
+	proto.RegisterType((*QueryChainLinkOwnersRequest)(nil), "desmos.profiles.v2.QueryChainLinkOwnersRequest")
+	proto.RegisterType((*QueryChainLinkOwnersResponse)(nil), "desmos.profiles.v2.QueryChainLinkOwnersResponse")
+	proto.RegisterType((*QueryChainLinkOwnersResponse_ChainLinkOwnerDetails)(nil), "desmos.profiles.v2.QueryChainLinkOwnersResponse.ChainLinkOwnerDetails")
 }
 
 func init() {
@@ -170,33 +361,37 @@ func init() {
 }
 
 var fileDescriptor_f3f86dba4268625b = []byte{
-	// 401 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x91, 0x41, 0x8e, 0xd3, 0x30,
-	0x14, 0x86, 0xe3, 0x99, 0x32, 0xd2, 0x78, 0x76, 0x16, 0x1a, 0x32, 0x11, 0x13, 0xaa, 0x59, 0x40,
-	0x55, 0x84, 0xad, 0xa6, 0x2b, 0xb6, 0x45, 0x82, 0x05, 0x08, 0x41, 0x96, 0x6c, 0x2a, 0xa7, 0x7d,
-	0x75, 0x23, 0x12, 0x3b, 0x8d, 0x9d, 0x88, 0xde, 0x82, 0x0b, 0x70, 0x02, 0x2e, 0xd2, 0x65, 0x97,
-	0xac, 0x10, 0x6a, 0x2f, 0x82, 0x62, 0x1b, 0x5a, 0x54, 0xa4, 0xd9, 0xbd, 0x3f, 0xff, 0xff, 0xf2,
-	0x7f, 0xc9, 0xc3, 0xc3, 0x39, 0xe8, 0x52, 0x69, 0x56, 0xd5, 0x6a, 0x91, 0x17, 0xa0, 0x59, 0x9b,
-	0xb0, 0x55, 0x03, 0xf5, 0x7a, 0x3a, 0x5b, 0xf2, 0x5c, 0x4e, 0x8b, 0x5c, 0x7e, 0xd6, 0xb4, 0xaa,
-	0x95, 0x51, 0x84, 0xb8, 0x2c, 0xfd, 0x93, 0xa5, 0x6d, 0x12, 0x3d, 0x14, 0x4a, 0x28, 0x6b, 0xb3,
-	0x6e, 0x72, 0xc9, 0xe8, 0xb1, 0x50, 0x4a, 0x14, 0xc0, 0x78, 0x95, 0x33, 0x2e, 0xa5, 0x32, 0xdc,
-	0xe4, 0x4a, 0xfa, 0xf7, 0x44, 0x37, 0xde, 0xb5, 0x2a, 0x6b, 0x16, 0x8c, 0xcb, 0xb5, 0xb7, 0x9e,
-	0xff, 0x07, 0xa7, 0x54, 0x73, 0x28, 0xf4, 0x29, 0x4f, 0x74, 0x33, 0x53, 0x5d, 0x78, 0xea, 0xea,
-	0x9d, 0xf0, 0xd6, 0xd0, 0x29, 0x96, 0x71, 0x0d, 0xee, 0x7b, 0x58, 0x3b, 0xca, 0xc0, 0xf0, 0x11,
-	0xab, 0xb8, 0xc8, 0xa5, 0xe5, 0x71, 0xd9, 0xbb, 0xef, 0x08, 0x5f, 0x7f, 0xec, 0x22, 0xaf, 0xba,
-	0x86, 0x77, 0x5d, 0x41, 0x0a, 0xab, 0x06, 0xb4, 0x21, 0x04, 0xf7, 0x1a, 0x0d, 0x75, 0x88, 0xfa,
-	0x68, 0x70, 0x99, 0xda, 0x99, 0xdc, 0x62, 0xec, 0x50, 0x24, 0x2f, 0x21, 0x3c, 0xb3, 0xce, 0xa5,
-	0x7d, 0xf2, 0x9e, 0x97, 0x40, 0xae, 0xf1, 0x85, 0xe1, 0xb5, 0x00, 0x13, 0x9e, 0x5b, 0xcb, 0x2b,
-	0xf2, 0x1a, 0xe3, 0x43, 0x73, 0xd8, 0xeb, 0xa3, 0xc1, 0x55, 0xf2, 0x94, 0x7a, 0xe8, 0x0e, 0x93,
-	0x5a, 0x4c, 0xea, 0x31, 0xe9, 0x07, 0x2e, 0xc0, 0x63, 0xa4, 0x47, 0x9b, 0x77, 0xdf, 0x10, 0x7e,
-	0x74, 0x42, 0xab, 0x2b, 0x25, 0x35, 0x90, 0x97, 0xf8, 0x81, 0xfd, 0x3f, 0x21, 0xea, 0x9f, 0x0f,
-	0xae, 0x92, 0x5b, 0x7a, 0x7a, 0x30, 0xfa, 0x77, 0x6d, 0xd2, 0xdb, 0xfc, 0x7c, 0x12, 0xa4, 0x6e,
-	0x83, 0xbc, 0xf9, 0x07, 0xef, 0xcc, 0xe2, 0x3d, 0xbb, 0x17, 0xcf, 0xf5, 0x1e, 0xf3, 0x4d, 0xde,
-	0x6e, 0x76, 0x31, 0xda, 0xee, 0x62, 0xf4, 0x6b, 0x17, 0xa3, 0xaf, 0xfb, 0x38, 0xd8, 0xee, 0xe3,
-	0xe0, 0xc7, 0x3e, 0x0e, 0x3e, 0x8d, 0x44, 0x6e, 0x96, 0x4d, 0x46, 0x67, 0xaa, 0x64, 0x0e, 0xec,
-	0x45, 0xc1, 0x33, 0xed, 0x67, 0xd6, 0x8e, 0xd9, 0x97, 0xc3, 0xdd, 0xcd, 0xba, 0x02, 0x9d, 0x5d,
-	0xd8, 0x0b, 0x8d, 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0x58, 0xf0, 0xc2, 0x74, 0xa6, 0x02, 0x00,
-	0x00,
+	// 475 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x93, 0x4f, 0x6f, 0xd3, 0x30,
+	0x18, 0x87, 0xeb, 0xb6, 0x54, 0x9a, 0x77, 0xb3, 0x60, 0x64, 0x61, 0x0b, 0xd5, 0x0e, 0x50, 0x0d,
+	0x61, 0xab, 0xdd, 0x89, 0xeb, 0x40, 0xe3, 0x00, 0xe2, 0x4f, 0x8f, 0x5c, 0x2a, 0xa7, 0x7d, 0x97,
+	0x59, 0x24, 0x76, 0x16, 0x3b, 0x85, 0x7e, 0x0b, 0x3e, 0x00, 0x7c, 0x02, 0x4e, 0x7c, 0x8b, 0x1d,
+	0x77, 0xe4, 0x84, 0x50, 0xfb, 0x45, 0x50, 0x6c, 0xb3, 0x11, 0x35, 0x0c, 0x09, 0xf5, 0xe6, 0xd7,
+	0xef, 0x6b, 0xff, 0x9e, 0x3e, 0x8d, 0xf1, 0xe1, 0x0c, 0x74, 0xa6, 0x34, 0xcb, 0x0b, 0x75, 0x2a,
+	0x52, 0xd0, 0x6c, 0x3e, 0x62, 0xe7, 0x25, 0x14, 0x8b, 0xc9, 0xf4, 0x8c, 0x0b, 0x39, 0x49, 0x85,
+	0x7c, 0xaf, 0x69, 0x5e, 0x28, 0xa3, 0x08, 0x71, 0xb3, 0xf4, 0xf7, 0x2c, 0x9d, 0x8f, 0xc2, 0xdb,
+	0x89, 0x4a, 0x94, 0x6d, 0xb3, 0x6a, 0xe5, 0x26, 0xc3, 0xbd, 0x44, 0xa9, 0x24, 0x05, 0xc6, 0x73,
+	0xc1, 0xb8, 0x94, 0xca, 0x70, 0x23, 0x94, 0xf4, 0xf7, 0x84, 0xbb, 0xbe, 0x6b, 0xab, 0xb8, 0x3c,
+	0x65, 0x5c, 0x2e, 0x7c, 0xeb, 0x51, 0x03, 0x4e, 0xa6, 0x66, 0x90, 0xea, 0x75, 0x9e, 0x70, 0x77,
+	0xaa, 0xaa, 0xe1, 0x89, 0x8b, 0x77, 0x85, 0x6f, 0x1d, 0xba, 0x8a, 0xc5, 0x5c, 0x83, 0xfb, 0x3d,
+	0x6c, 0x3e, 0x8c, 0xc1, 0xf0, 0x21, 0xcb, 0x79, 0x22, 0xa4, 0xe5, 0x71, 0xb3, 0x07, 0x5f, 0x11,
+	0xde, 0x79, 0x5b, 0x8d, 0x3c, 0xad, 0x12, 0x5e, 0x56, 0x01, 0x63, 0x38, 0x2f, 0x41, 0x1b, 0x42,
+	0x70, 0xb7, 0xd4, 0x50, 0x04, 0xa8, 0x8f, 0x06, 0x5b, 0x63, 0xbb, 0x26, 0xfb, 0x18, 0x3b, 0x14,
+	0xc9, 0x33, 0x08, 0xda, 0xb6, 0xb3, 0x65, 0x77, 0x5e, 0xf1, 0x0c, 0xc8, 0x0e, 0xee, 0x19, 0x5e,
+	0x24, 0x60, 0x82, 0x8e, 0x6d, 0xf9, 0x8a, 0x9c, 0x60, 0x7c, 0x9d, 0x1c, 0x74, 0xfb, 0x68, 0xb0,
+	0x3d, 0x7a, 0x40, 0x3d, 0x74, 0x85, 0x49, 0x2d, 0x26, 0xf5, 0x98, 0xf4, 0x0d, 0x4f, 0xc0, 0x63,
+	0x8c, 0xff, 0x38, 0x79, 0xf0, 0x05, 0xe1, 0xbb, 0x6b, 0xb4, 0x3a, 0x57, 0x52, 0x03, 0x79, 0x82,
+	0x6f, 0x59, 0x3f, 0x01, 0xea, 0x77, 0x06, 0xdb, 0xa3, 0x7d, 0xba, 0xfe, 0x87, 0xd1, 0xab, 0x63,
+	0xc7, 0xdd, 0x8b, 0x1f, 0xf7, 0x5b, 0x63, 0x77, 0x82, 0x3c, 0xaf, 0xe1, 0xb5, 0x2d, 0xde, 0xc3,
+	0x7f, 0xe2, 0xb9, 0xdc, 0x1a, 0xdf, 0x67, 0x84, 0xef, 0xd5, 0xf9, 0x5e, 0x7f, 0x90, 0x50, 0x5c,
+	0x29, 0xad, 0xeb, 0x43, 0x7f, 0xd7, 0xd7, 0xbe, 0x41, 0x5f, 0xe7, 0xbf, 0xf5, 0x7d, 0x6b, 0xe3,
+	0xbd, 0x66, 0x3c, 0xef, 0x70, 0x86, 0x7b, 0xca, 0xee, 0x78, 0x89, 0x27, 0x4d, 0x12, 0x6f, 0xba,
+	0x81, 0xd6, 0xf7, 0x9f, 0x81, 0xe1, 0x22, 0xd5, 0xde, 0xb6, 0xbf, 0x7b, 0x63, 0xba, 0xc3, 0x18,
+	0xdf, 0x69, 0xcc, 0xdb, 0xe0, 0xa7, 0x7b, 0xfc, 0xe2, 0x62, 0x19, 0xa1, 0xcb, 0x65, 0x84, 0x7e,
+	0x2e, 0x23, 0xf4, 0x69, 0x15, 0xb5, 0x2e, 0x57, 0x51, 0xeb, 0xfb, 0x2a, 0x6a, 0xbd, 0x1b, 0x26,
+	0xc2, 0x9c, 0x95, 0x31, 0x9d, 0xaa, 0x8c, 0x39, 0x4d, 0x8f, 0x53, 0x1e, 0x6b, 0xbf, 0x66, 0xf3,
+	0x23, 0xf6, 0xf1, 0xfa, 0x29, 0x9b, 0x45, 0x0e, 0x3a, 0xee, 0xd9, 0x47, 0x77, 0xf4, 0x2b, 0x00,
+	0x00, 0xff, 0xff, 0xb5, 0xe1, 0xdb, 0xd4, 0x79, 0x04, 0x00, 0x00,
 }
 
 func (m *QueryChainLinksRequest) Marshal() (dAtA []byte, err error) {
@@ -304,6 +499,148 @@ func (m *QueryChainLinksResponse) MarshalToSizedBuffer(dAtA []byte) (int, error)
 	return len(dAtA) - i, nil
 }
 
+func (m *QueryChainLinkOwnersRequest) 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 *QueryChainLinkOwnersRequest) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryChainLinkOwnersRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.Pagination != nil {
+		{
+			size, err := m.Pagination.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintQueryChainLinks(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x1a
+	}
+	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] = 0x12
+	}
+	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] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *QueryChainLinkOwnersResponse) 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 *QueryChainLinkOwnersResponse) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryChainLinkOwnersResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.Pagination != nil {
+		{
+			size, err := m.Pagination.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintQueryChainLinks(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x12
+	}
+	if len(m.Owners) > 0 {
+		for iNdEx := len(m.Owners) - 1; iNdEx >= 0; iNdEx-- {
+			{
+				size, err := m.Owners[iNdEx].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 *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) 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 *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) 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 encodeVarintQueryChainLinks(dAtA []byte, offset int, v uint64) int {
 	offset -= sovQueryChainLinks(v)
 	base := offset
@@ -359,6 +696,67 @@ func (m *QueryChainLinksResponse) Size() (n int) {
 	return n
 }
 
+func (m *QueryChainLinkOwnersRequest) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = 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))
+	}
+	if m.Pagination != nil {
+		l = m.Pagination.Size()
+		n += 1 + l + sovQueryChainLinks(uint64(l))
+	}
+	return n
+}
+
+func (m *QueryChainLinkOwnersResponse) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if len(m.Owners) > 0 {
+		for _, e := range m.Owners {
+			l = e.Size()
+			n += 1 + l + sovQueryChainLinks(uint64(l))
+		}
+	}
+	if m.Pagination != nil {
+		l = m.Pagination.Size()
+		n += 1 + l + sovQueryChainLinks(uint64(l))
+	}
+	return n
+}
+
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) 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 sovQueryChainLinks(x uint64) (n int) {
 	return (math_bits.Len64(x|1) + 6) / 7
 }
@@ -388,17 +786,199 @@ func (m *QueryChainLinksRequest) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: QueryChainLinksRequest: wiretype end group for non-group")
+			return fmt.Errorf("proto: QueryChainLinksRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryChainLinksRequest: 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
+		case 4:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Pagination", 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 m.Pagination == nil {
+				m.Pagination = &query.PageRequest{}
+			}
+			if err := m.Pagination.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 *QueryChainLinksResponse) 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: QueryChainLinksResponse: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: QueryChainLinksRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: QueryChainLinksResponse: 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)
+				return fmt.Errorf("proto: wrong wireType = %d for field Links", wireType)
 			}
-			var stringLen uint64
+			var msglen int
 			for shift := uint(0); ; shift += 7 {
 				if shift >= 64 {
 					return ErrIntOverflowQueryChainLinks
@@ -408,25 +988,113 @@ func (m *QueryChainLinksRequest) Unmarshal(dAtA []byte) error {
 				}
 				b := dAtA[iNdEx]
 				iNdEx++
-				stringLen |= uint64(b&0x7F) << shift
+				msglen |= int(b&0x7F) << shift
 				if b < 0x80 {
 					break
 				}
 			}
-			intStringLen := int(stringLen)
-			if intStringLen < 0 {
+			if msglen < 0 {
 				return ErrInvalidLengthQueryChainLinks
 			}
-			postIndex := iNdEx + intStringLen
+			postIndex := iNdEx + msglen
 			if postIndex < 0 {
 				return ErrInvalidLengthQueryChainLinks
 			}
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.User = string(dAtA[iNdEx:postIndex])
+			m.Links = append(m.Links, ChainLink{})
+			if err := m.Links[len(m.Links)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
 			iNdEx = postIndex
 		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Pagination", 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 m.Pagination == nil {
+				m.Pagination = &query.PageResponse{}
+			}
+			if err := m.Pagination.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 *QueryChainLinkOwnersRequest) 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: QueryChainLinkOwnersRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: QueryChainLinkOwnersRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field ChainName", wireType)
 			}
@@ -458,7 +1126,7 @@ func (m *QueryChainLinksRequest) Unmarshal(dAtA []byte) error {
 			}
 			m.ChainName = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
-		case 3:
+		case 2:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Target", wireType)
 			}
@@ -490,7 +1158,7 @@ func (m *QueryChainLinksRequest) Unmarshal(dAtA []byte) error {
 			}
 			m.Target = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
-		case 4:
+		case 3:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Pagination", wireType)
 			}
@@ -547,7 +1215,7 @@ func (m *QueryChainLinksRequest) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *QueryChainLinksResponse) Unmarshal(dAtA []byte) error {
+func (m *QueryChainLinkOwnersResponse) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -570,15 +1238,15 @@ func (m *QueryChainLinksResponse) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: QueryChainLinksResponse: wiretype end group for non-group")
+			return fmt.Errorf("proto: QueryChainLinkOwnersResponse: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: QueryChainLinksResponse: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: QueryChainLinkOwnersResponse: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
 		case 1:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Links", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field Owners", wireType)
 			}
 			var msglen int
 			for shift := uint(0); ; shift += 7 {
@@ -605,8 +1273,8 @@ func (m *QueryChainLinksResponse) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Links = append(m.Links, ChainLink{})
-			if err := m.Links[len(m.Links)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+			m.Owners = append(m.Owners, QueryChainLinkOwnersResponse_ChainLinkOwnerDetails{})
+			if err := m.Owners[len(m.Owners)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
 			iNdEx = postIndex
@@ -667,6 +1335,152 @@ func (m *QueryChainLinksResponse) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *QueryChainLinkOwnersResponse_ChainLinkOwnerDetails) 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: ChainLinkOwnerDetails: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: ChainLinkOwnerDetails: 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 skipQueryChainLinks(dAtA []byte) (n int, err error) {
 	l := len(dAtA)
 	iNdEx := 0


Test patch:
diff --git a/testutil/profiles.go b/testutil/profiles.go
index 828fd2bb..e8561857 100644
--- a/testutil/profiles.go
+++ b/testutil/profiles.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/cosmos/cosmos-sdk/codec"
+
 	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
 	cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
 	sdk "github.com/cosmos/cosmos-sdk/types"
@@ -48,6 +50,15 @@ func PubKeyFromBech32(pubKey string) cryptotypes.PubKey {
 	return publicKey
 }
 
+func PubKeyFromJSON(cdc codec.Codec, pubKey string) cryptotypes.PubKey {
+	var publicKey cryptotypes.PubKey
+	err := cdc.UnmarshalInterfaceJSON([]byte(pubKey), &publicKey)
+	if err != nil {
+		panic(err)
+	}
+	return publicKey
+}
+
 func ProfileFromAddr(address string) *types.Profile {
 	profile, err := types.NewProfile(
 		fmt.Sprintf("%s-dtag", address),
diff --git a/x/profiles/keeper/grpc_query_test.go b/x/profiles/keeper/grpc_query_test.go
index aee28a8c..f7c5ea54 100644
--- a/x/profiles/keeper/grpc_query_test.go
+++ b/x/profiles/keeper/grpc_query_test.go
@@ -518,6 +518,151 @@ func (suite *KeeperTestSuite) TestQueryServer_ChainLinks() {
 	}
 }
 
+func (suite *KeeperTestSuite) TestQueryServer_ChainLinkOwners() {
+	testCases := []struct {
+		name      string
+		store     func(ctx sdk.Context)
+		request   *types.QueryChainLinkOwnersRequest
+		shouldErr bool
+		expOwners []types.QueryChainLinkOwnersResponse_ChainLinkOwnerDetails
+	}{
+		{
+			name: "query without any data returns everything",
+			store: func(ctx sdk.Context) {
+				suite.Require().NoError(suite.k.SaveProfile(ctx, testutil.ProfileFromAddr("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47")))
+				suite.Require().NoError(suite.k.SaveChainLink(ctx, types.NewChainLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					types.NewBech32Address("cosmos1nc54z3kzyal57w6wcf5khmwrxx5rafnwvu0m5z", "cosmos"),
+					types.NewProof(
+						testutil.PubKeyFromBech32("cosmospub1addwnpepqvryxhhqhw52c4ny5twtfzf3fsrjqhx0x5cuya0fylw0wu0eqptykeqhr4d"),
+						testutil.SingleSignatureProtoFromHex("909e38994b1583d3f14384c2e9a03c90064e8fd8e19b780bb0ba303dfe671a27287da04d0ce096ce9a140bd070ee36818f5519eb2070a16971efd8143855524b"),
+						"74657874",
+					),
+					types.NewChainConfig("cosmos"),
+					time.Date(2019, 1, 1, 00, 00, 00, 000, time.UTC),
+				)))
+				suite.Require().NoError(suite.k.SaveChainLink(ctx, types.NewChainLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					types.NewBech32Address("cosmos167x6ehhple8gwz5ezy9x0464jltvdpzl6qfdt4", "cosmos"),
+					types.NewProof(
+						testutil.PubKeyFromJSON(suite.cdc, `{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A6XZ3b2QhhtHrglAqacSwnsastcmqdQ0OuGHXfA3H43c"}`),
+						testutil.SingleSignatureProtoFromHex("fc71bae3b7f55105d612d84f93738f4efdc32f866198c37b48d9e986d27082ba6162de59586cc53ab188870024fb8aad41b1407857787ddc9d0110695d8ffb54"),
+						"74657874",
+					),
+					types.NewChainConfig("likecoin"),
+					time.Date(2019, 1, 1, 00, 00, 00, 000, time.UTC),
+				)))
+			},
+			request:   types.NewQueryChainLinkOwnersRequest("", "", nil),
+			shouldErr: false,
+			expOwners: []types.QueryChainLinkOwnersResponse_ChainLinkOwnerDetails{
+				{
+					User:      "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					ChainName: "cosmos",
+					Target:    "cosmos1nc54z3kzyal57w6wcf5khmwrxx5rafnwvu0m5z",
+				},
+				{
+					User:      "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					ChainName: "likecoin",
+					Target:    "cosmos167x6ehhple8gwz5ezy9x0464jltvdpzl6qfdt4",
+				},
+			},
+		},
+		{
+			name: "query with chain name returns the correct data",
+			store: func(ctx sdk.Context) {
+				suite.Require().NoError(suite.k.SaveProfile(ctx, testutil.ProfileFromAddr("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47")))
+				suite.Require().NoError(suite.k.SaveChainLink(ctx, types.NewChainLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					types.NewBech32Address("cosmos1nc54z3kzyal57w6wcf5khmwrxx5rafnwvu0m5z", "cosmos"),
+					types.NewProof(
+						testutil.PubKeyFromBech32("cosmospub1addwnpepqvryxhhqhw52c4ny5twtfzf3fsrjqhx0x5cuya0fylw0wu0eqptykeqhr4d"),
+						testutil.SingleSignatureProtoFromHex("909e38994b1583d3f14384c2e9a03c90064e8fd8e19b780bb0ba303dfe671a27287da04d0ce096ce9a140bd070ee36818f5519eb2070a16971efd8143855524b"),
+						"74657874",
+					),
+					types.NewChainConfig("cosmos"),
+					time.Date(2019, 1, 1, 00, 00, 00, 000, time.UTC),
+				)))
+				suite.Require().NoError(suite.k.SaveChainLink(ctx, types.NewChainLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					types.NewBech32Address("cosmos167x6ehhple8gwz5ezy9x0464jltvdpzl6qfdt4", "cosmos"),
+					types.NewProof(
+						testutil.PubKeyFromJSON(suite.cdc, `{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A6XZ3b2QhhtHrglAqacSwnsastcmqdQ0OuGHXfA3H43c"}`),
+						testutil.SingleSignatureProtoFromHex("fc71bae3b7f55105d612d84f93738f4efdc32f866198c37b48d9e986d27082ba6162de59586cc53ab188870024fb8aad41b1407857787ddc9d0110695d8ffb54"),
+						"74657874",
+					),
+					types.NewChainConfig("likecoin"),
+					time.Date(2019, 1, 1, 00, 00, 00, 000, time.UTC),
+				)))
+			},
+			request:   types.NewQueryChainLinkOwnersRequest("cosmos", "", nil),
+			shouldErr: false,
+			expOwners: []types.QueryChainLinkOwnersResponse_ChainLinkOwnerDetails{
+				{
+					User:      "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					ChainName: "cosmos",
+					Target:    "cosmos1nc54z3kzyal57w6wcf5khmwrxx5rafnwvu0m5z",
+				},
+			},
+		},
+		{
+			name: "query with chain name and target returns the correct data",
+			store: func(ctx sdk.Context) {
+				suite.Require().NoError(suite.k.SaveProfile(ctx, testutil.ProfileFromAddr("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47")))
+				suite.Require().NoError(suite.k.SaveChainLink(ctx, types.NewChainLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					types.NewBech32Address("cosmos1nc54z3kzyal57w6wcf5khmwrxx5rafnwvu0m5z", "cosmos"),
+					types.NewProof(
+						testutil.PubKeyFromBech32("cosmospub1addwnpepqvryxhhqhw52c4ny5twtfzf3fsrjqhx0x5cuya0fylw0wu0eqptykeqhr4d"),
+						testutil.SingleSignatureProtoFromHex("909e38994b1583d3f14384c2e9a03c90064e8fd8e19b780bb0ba303dfe671a27287da04d0ce096ce9a140bd070ee36818f5519eb2070a16971efd8143855524b"),
+						"74657874",
+					),
+					types.NewChainConfig("cosmos"),
+					time.Date(2019, 1, 1, 00, 00, 00, 000, time.UTC),
+				)))
+				suite.Require().NoError(suite.k.SaveChainLink(ctx, types.NewChainLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					types.NewBech32Address("cosmos167x6ehhple8gwz5ezy9x0464jltvdpzl6qfdt4", "cosmos"),
+					types.NewProof(
+						testutil.PubKeyFromJSON(suite.cdc, `{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A6XZ3b2QhhtHrglAqacSwnsastcmqdQ0OuGHXfA3H43c"}`),
+						testutil.SingleSignatureProtoFromHex("fc71bae3b7f55105d612d84f93738f4efdc32f866198c37b48d9e986d27082ba6162de59586cc53ab188870024fb8aad41b1407857787ddc9d0110695d8ffb54"),
+						"74657874",
+					),
+					types.NewChainConfig("likecoin"),
+					time.Date(2019, 1, 1, 00, 00, 00, 000, time.UTC),
+				)))
+			},
+			request:   types.NewQueryChainLinkOwnersRequest("cosmos", "cosmos1nc54z3kzyal57w6wcf5khmwrxx5rafnwvu0m5z", nil),
+			shouldErr: false,
+			expOwners: []types.QueryChainLinkOwnersResponse_ChainLinkOwnerDetails{
+				{
+					User:      "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					ChainName: "cosmos",
+					Target:    "cosmos1nc54z3kzyal57w6wcf5khmwrxx5rafnwvu0m5z",
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		suite.Run(tc.name, func() {
+			ctx, _ := suite.ctx.CacheContext()
+			if tc.store != nil {
+				tc.store(ctx)
+			}
+
+			res, err := suite.k.ChainLinkOwners(sdk.WrapSDKContext(ctx), tc.request)
+			if tc.shouldErr {
+				suite.Require().Error(err)
+			} else {
+				suite.Require().NoError(err)
+				suite.Require().Equal(tc.expOwners, res.Owners)
+			}
+		})
+	}
+}
+
 func (suite *KeeperTestSuite) TestQueryServer_ApplicationLinks() {
 	testCases := []struct {
 		name                string
@@ -930,6 +1075,273 @@ func (suite *KeeperTestSuite) TestQueryServer_ApplicationLinkByClientID() {
 	}
 }
 
+func (suite *KeeperTestSuite) TestQueryServer_ApplicationLinkOwners() {
+	testCases := []struct {
+		name      string
+		store     func(ctx sdk.Context)
+		req       *types.QueryApplicationLinkOwnersRequest
+		shouldErr bool
+		expOwners []types.QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails
+	}{
+		{
+			name: "query without any data returns everything",
+			store: func(ctx sdk.Context) {
+				profile := testutil.ProfileFromAddr("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+						types.NewData("twitter", "twitter_user"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							0,
+							1,
+							types.NewOracleRequestCallData(
+								"twitter",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+
+				profile = testutil.ProfileFromAddr("cosmos1ngzeux3j0vfkps0779y0c8pnrmszlg0hekp5um")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1ngzeux3j0vfkps0779y0c8pnrmszlg0hekp5um",
+						types.NewData("github", "github_user"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							0,
+							1,
+							types.NewOracleRequestCallData(
+								"twitter",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+			},
+			req:       types.NewQueryApplicationLinkOwnersRequest("", "", nil),
+			shouldErr: false,
+			expOwners: []types.QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails{
+				{
+					User:        "cosmos1ngzeux3j0vfkps0779y0c8pnrmszlg0hekp5um",
+					Application: "github",
+					Username:    "github_user",
+				},
+				{
+					User:        "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					Application: "twitter",
+					Username:    "twitter_user",
+				},
+			},
+		},
+		{
+			name: "query with application returns the correct data",
+			store: func(ctx sdk.Context) {
+				profile := testutil.ProfileFromAddr("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+						types.NewData("twitter", "first_user"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							0,
+							1,
+							types.NewOracleRequestCallData(
+								"twitter",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+
+				profile = testutil.ProfileFromAddr("cosmos1ngzeux3j0vfkps0779y0c8pnrmszlg0hekp5um")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1ngzeux3j0vfkps0779y0c8pnrmszlg0hekp5um",
+						types.NewData("twitter", "second_user"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							0,
+							1,
+							types.NewOracleRequestCallData(
+								"twitter",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+
+				profile = testutil.ProfileFromAddr("cosmos1pxsak5c7ke5tz3d8alawuzu3cayr9s65ce7njr")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1pxsak5c7ke5tz3d8alawuzu3cayr9s65ce7njr",
+						types.NewData("github", "second_user"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							0,
+							1,
+							types.NewOracleRequestCallData(
+								"github",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+			},
+			req:       types.NewQueryApplicationLinkOwnersRequest("twitter", "", nil),
+			shouldErr: false,
+			expOwners: []types.QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails{
+				{
+					User:        "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					Application: "twitter",
+					Username:    "first_user",
+				},
+				{
+					User:        "cosmos1ngzeux3j0vfkps0779y0c8pnrmszlg0hekp5um",
+					Application: "twitter",
+					Username:    "second_user",
+				},
+			},
+		},
+		{
+			name: "query with application and username returns the correct data",
+			store: func(ctx sdk.Context) {
+				profile := testutil.ProfileFromAddr("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+						types.NewData("twitter", "user"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							0,
+							1,
+							types.NewOracleRequestCallData(
+								"twitter",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+
+				profile = testutil.ProfileFromAddr("cosmos1ngzeux3j0vfkps0779y0c8pnrmszlg0hekp5um")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1ngzeux3j0vfkps0779y0c8pnrmszlg0hekp5um",
+						types.NewData("twitter", "user"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							0,
+							1,
+							types.NewOracleRequestCallData(
+								"twitter",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+
+				profile = testutil.ProfileFromAddr("cosmos1mrmyggajlv0k3mlrhergjjnt75srn5y5u5a83x")
+				suite.ak.SetAccount(ctx, profile)
+
+				suite.Require().NoError(suite.k.SaveApplicationLink(
+					ctx,
+					types.NewApplicationLink(
+						"cosmos1mrmyggajlv0k3mlrhergjjnt75srn5y5u5a83x",
+						types.NewData("twitter", "second_user"),
+						types.ApplicationLinkStateInitialized,
+						types.NewOracleRequest(
+							0,
+							1,
+							types.NewOracleRequestCallData(
+								"twitter",
+								"7B22757365726E616D65223A22526963636172646F4D222C22676973745F6964223A223732306530303732333930613930316262383065353966643630643766646564227D",
+							),
+							"client_id",
+						),
+						nil,
+						time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					)),
+				)
+			},
+			req:       types.NewQueryApplicationLinkOwnersRequest("twitter", "user", nil),
+			shouldErr: false,
+			expOwners: []types.QueryApplicationLinkOwnersResponse_ApplicationLinkOwnerDetails{
+				{
+					User:        "cosmos1ngzeux3j0vfkps0779y0c8pnrmszlg0hekp5um",
+					Application: "twitter",
+					Username:    "user",
+				},
+				{
+					User:        "cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					Application: "twitter",
+					Username:    "user",
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		suite.Run(tc.name, func() {
+			ctx, _ := suite.ctx.CacheContext()
+			if tc.store != nil {
+				tc.store(ctx)
+			}
+
+			res, err := suite.k.ApplicationLinkOwners(sdk.WrapSDKContext(ctx), tc.req)
+			if tc.shouldErr {
+				suite.Require().Error(err)
+			} else {
+				suite.Require().NoError(err)
+				suite.Require().Equal(tc.expOwners, res.Owners)
+			}
+		})
+	}
+}
+
 func (suite *KeeperTestSuite) TestQueryServer_Params() {
 	ctx, _ := suite.ctx.CacheContext()
 
diff --git a/x/profiles/keeper/keeper_app_links_test.go b/x/profiles/keeper/keeper_app_links_test.go
index 934503e2..fb71f4f2 100644
--- a/x/profiles/keeper/keeper_app_links_test.go
+++ b/x/profiles/keeper/keeper_app_links_test.go
@@ -16,6 +16,7 @@ func (suite *KeeperTestSuite) Test_SaveApplicationLink() {
 		store     func(ctx sdk.Context)
 		link      types.ApplicationLink
 		shouldErr bool
+		check     func(ctx sdk.Context)
 	}{
 		{
 			name: "user without profile returns error",
@@ -54,6 +55,22 @@ func (suite *KeeperTestSuite) Test_SaveApplicationLink() {
 				time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
 			),
 			shouldErr: false,
+			check: func(ctx sdk.Context) {
+				suite.Require().True(suite.k.HasApplicationLink(ctx,
+					"cosmos10nsdxxdvy9qka3zv0lzw8z9cnu6kanld8jh773",
+					"twitter",
+					"twitteruser",
+				))
+
+				// Check the additional keys
+				store := ctx.KVStore(suite.storeKey)
+				suite.Require().True(store.Has(types.ApplicationLinkClientIDKey("client_id")))
+				suite.Require().True(store.Has(types.ApplicationLinkOwnerKey(
+					"twitter",
+					"twitteruser",
+					"cosmos10nsdxxdvy9qka3zv0lzw8z9cnu6kanld8jh773",
+				)))
+			},
 		},
 	}
 
@@ -70,10 +87,9 @@ func (suite *KeeperTestSuite) Test_SaveApplicationLink() {
 				suite.Require().Error(err)
 			} else {
 				suite.Require().NoError(err)
-
-				store := ctx.KVStore(suite.storeKey)
-				suite.Require().True(store.Has(types.UserApplicationLinkKey(tc.link.User, tc.link.Data.Application, tc.link.Data.Username)))
-				suite.Require().True(store.Has(types.ApplicationLinkClientIDKey(tc.link.OracleRequest.ClientID)))
+				if tc.check != nil {
+					tc.check(ctx)
+				}
 			}
 		})
 	}
@@ -487,6 +503,15 @@ func (suite *KeeperTestSuite) Test_DeleteApplicationLink() {
 					"twitter",
 					"twitteruser",
 				))
+
+				// Check the additional keys
+				store := ctx.KVStore(suite.storeKey)
+				suite.Require().False(store.Has(types.ApplicationLinkClientIDKey("client_id")))
+				suite.Require().False(store.Has(types.ApplicationLinkOwnerKey(
+					"twitter",
+					"twitteruser",
+					"cosmos10nsdxxdvy9qka3zv0lzw8z9cnu6kanld8jh773",
+				)))
 			},
 		},
 	}
diff --git a/x/profiles/keeper/keeper_chain_links_test.go b/x/profiles/keeper/keeper_chain_links_test.go
index 89e589e1..72e18b09 100644
--- a/x/profiles/keeper/keeper_chain_links_test.go
+++ b/x/profiles/keeper/keeper_chain_links_test.go
@@ -15,7 +15,7 @@ import (
 	"github.com/desmos-labs/desmos/v3/x/profiles/types"
 )
 
-func (suite *KeeperTestSuite) TestKeeper_StoreChainLink() {
+func (suite *KeeperTestSuite) TestKeeper_SaveChainLink() {
 	// Generate source and destination key
 	ext := suite.GetRandomProfile()
 	sig := hex.EncodeToString(ext.Sign([]byte(ext.GetAddress().String())))
@@ -143,6 +143,14 @@ func (suite *KeeperTestSuite) TestKeeper_StoreChainLink() {
 					types.NewChainConfig("cosmos"),
 					time.Date(2020, 1, 2, 00, 00, 00, 000, time.UTC),
 				))
+
+				// Check the additional keys
+				store := ctx.KVStore(suite.storeKey)
+				suite.Require().True(store.Has(types.ChainLinkOwnerKey(
+					"cosmos",
+					ext.GetAddress().String(),
+					"cosmos19xz3mrvzvp9ymgmudhpukucg6668l5haakh04x",
+				)))
 			},
 		},
 	}
@@ -355,6 +363,14 @@ func (suite *KeeperTestSuite) TestKeeper_DeleteChainLink() {
 					"cosmos",
 					ext.GetAddress().String(),
 				))
+
+				// Check the additional keys
+				store := ctx.KVStore(suite.storeKey)
+				suite.Require().False(store.Has(types.ChainLinkOwnerKey(
+					"cosmos",
+					ext.GetAddress().String(),
+					"cosmos10nsdxxdvy9qka3zv0lzw8z9cnu6kanld8jh773",
+				)))
 			},
 		},
 	}
diff --git a/x/profiles/keeper/keeper_test.go b/x/profiles/keeper/keeper_test.go
index 09e7cfed..40e60412 100644
--- a/x/profiles/keeper/keeper_test.go
+++ b/x/profiles/keeper/keeper_test.go
@@ -11,7 +11,7 @@ import (
 	"github.com/desmos-labs/desmos/v3/x/profiles/types"
 )
 
-func (suite *KeeperTestSuite) TestKeeper_StoreProfile() {
+func (suite *KeeperTestSuite) TestKeeper_SaveProfile() {
 	testCases := []struct {
 		name      string
 		store     func(ctx sdk.Context)
diff --git a/x/profiles/legacy/v5/store_test.go b/x/profiles/legacy/v5/store_test.go
new file mode 100644
index 00000000..9e6f1b36
--- /dev/null
+++ b/x/profiles/legacy/v5/store_test.go
@@ -0,0 +1,173 @@
+package v5_test
+
+import (
+	"encoding/hex"
+	"testing"
+	"time"
+
+	v5 "github.com/desmos-labs/desmos/v3/x/profiles/legacy/v5"
+
+	"github.com/cosmos/cosmos-sdk/types/tx/signing"
+
+	"github.com/desmos-labs/desmos/v3/testutil"
+	profilestypes "github.com/desmos-labs/desmos/v3/x/profiles/types"
+
+	"github.com/cosmos/cosmos-sdk/store"
+	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
+	capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
+	paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
+	"github.com/tendermint/tendermint/libs/log"
+	tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
+	dbm "github.com/tendermint/tm-db"
+
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	"github.com/stretchr/testify/require"
+
+	"github.com/desmos-labs/desmos/v3/app"
+	"github.com/desmos-labs/desmos/v3/x/relationships/types"
+)
+
+func buildContext(
+	keys map[string]*sdk.KVStoreKey, tKeys map[string]*sdk.TransientStoreKey, memKeys map[string]*sdk.MemoryStoreKey,
+) sdk.Context {
+	db := dbm.NewMemDB()
+	cms := store.NewCommitMultiStore(db)
+	for _, key := range keys {
+		cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db)
+	}
+	for _, tKey := range tKeys {
+		cms.MountStoreWithDB(tKey, sdk.StoreTypeTransient, db)
+	}
+	for _, memKey := range memKeys {
+		cms.MountStoreWithDB(memKey, sdk.StoreTypeMemory, nil)
+	}
+
+	err := cms.LoadLatestVersion()
+	if err != nil {
+		panic(err)
+	}
+
+	return sdk.NewContext(cms, tmproto.Header{}, false, log.NewNopLogger())
+}
+
+func TestMigrateStore(t *testing.T) {
+	cdc, _ := app.MakeCodecs()
+
+	// Build all the necessary keys
+	keys := sdk.NewKVStoreKeys(authtypes.StoreKey, types.StoreKey)
+	tKeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey)
+	memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey)
+
+	// Build common data
+	pubKey := testutil.PubKeyFromBech32("cosmospub1addwnpepqvryxhhqhw52c4ny5twtfzf3fsrjqhx0x5cuya0fylw0wu0eqptykeqhr4d")
+	pubKeyAny := testutil.NewAny(pubKey)
+
+	testCases := []struct {
+		name      string
+		store     func(ctx sdk.Context)
+		shouldErr bool
+		check     func(ctx sdk.Context)
+	}{
+		{
+			name: "application links owners are added properly",
+			store: func(ctx sdk.Context) {
+				kvStore := ctx.KVStore(keys[types.StoreKey])
+
+				// Store an application link
+				linkKey := profilestypes.UserApplicationLinkKey("cosmos10nsdxxdvy9qka3zv0lzw8z9cnu6kanld8jh773", "twitter", "twitteruser")
+				kvStore.Set(
+					linkKey,
+					cdc.MustMarshal(&profilestypes.ApplicationLink{
+						User:  "cosmos10nsdxxdvy9qka3zv0lzw8z9cnu6kanld8jh773",
+						Data:  profilestypes.NewData("twitter", "twitteruser"),
+						State: profilestypes.ApplicationLinkStateInitialized,
+						OracleRequest: profilestypes.NewOracleRequest(
+							0,
+							1,
+							profilestypes.NewOracleRequestCallData("twitter", "calldata"),
+							"client_id",
+						),
+						Result:       nil,
+						CreationTime: time.Date(2020, 1, 1, 00, 00, 00, 000, time.UTC),
+					}),
+				)
+
+				// Store an application link client id
+				kvStore.Set(profilestypes.ApplicationLinkClientIDKey("client_id"), linkKey)
+			},
+			check: func(ctx sdk.Context) {
+				kvStore := ctx.KVStore(keys[types.StoreKey])
+
+				// Check the application link owner
+				key := profilestypes.ApplicationLinkOwnerKey(
+					"twitter",
+					"twitteruser",
+					"cosmos10nsdxxdvy9qka3zv0lzw8z9cnu6kanld8jh773",
+				)
+				require.Equal(t, []byte{0x01}, kvStore.Get(key))
+			},
+		},
+		{
+			name: "chain link owners are added properly",
+			store: func(ctx sdk.Context) {
+				kvStore := ctx.KVStore(keys[types.StoreKey])
+
+				// Store the chain link
+				signatureValue, err := hex.DecodeString("7369676E6174757265")
+				require.NoError(t, err)
+				signature := profilestypes.SingleSignatureData{
+					Mode:      signing.SignMode_SIGN_MODE_TEXTUAL,
+					Signature: signatureValue,
+				}
+				signatureAny := testutil.NewAny(&signature)
+
+				chainLink := profilestypes.NewChainLink(
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+					profilestypes.NewBech32Address("cosmos10clxpupsmddtj7wu7g0wdysajqwp890mva046f", "cosmos"),
+					profilestypes.Proof{
+						PubKey:    pubKeyAny,
+						Signature: signatureAny,
+						PlainText: "74657874",
+					},
+					profilestypes.ChainConfig{Name: "cosmos"},
+					time.Date(2020, 1, 2, 00, 00, 00, 000, time.UTC),
+				)
+
+				kvStore.Set(
+					profilestypes.ChainLinksStoreKey("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47", "cosmos", "cosmos10clxpupsmddtj7wu7g0wdysajqwp890mva046f"),
+					cdc.MustMarshal(&chainLink),
+				)
+			},
+			check: func(ctx sdk.Context) {
+				kvStore := ctx.KVStore(keys[types.StoreKey])
+
+				key := profilestypes.ChainLinkOwnerKey(
+					"cosmos",
+					"cosmos10clxpupsmddtj7wu7g0wdysajqwp890mva046f",
+					"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47",
+				)
+				require.Equal(t, []byte{0x01}, kvStore.Get(key))
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := buildContext(keys, tKeys, memKeys)
+			if tc.store != nil {
+				tc.store(ctx)
+			}
+
+			err := v5.MigrateStore(ctx, keys[types.StoreKey], cdc)
+			if tc.shouldErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				if tc.check != nil {
+					tc.check(ctx)
+				}
+			}
+		})
+	}
+}