Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions docs/rfcs/0011-contacts-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# RFC-0011: Contacts API

| | |
| --------------- | --------------------------------------------------------------- |
| **Start Date** | 2026-04-17 |
| **Description** | Expose the user's contact list to products via Host API |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: via TruAPI no?

| **Authors** | Filippo Vecchiato |

## Summary

Products can read the user's host-managed address book. Each contact pairs local metadata with a context-scoped map keyed by `ProductAccountId` (`DotNsIdentifier` + `DerivationIndex`) — the same namespace used for Ring VRF alias derivation. By default a product only sees entries for its own context; cross-context access is a separate privilege.

## Motivation

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should start by the fact our privacy model gives each user a different alias and account in each context, so no single handle identifies a person across products. A contact list matters, because it is one very convenient way for each user to store a private notebook of mappings of local name ("Alice") to whichever identifier represents her in each product.


Products need to resolve human-readable identities to accounts. The host manages an address book but does not expose it. Without this API, users must paste raw keys or scan QR codes for every interaction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is trying to solve "human-readable identities", but letting products leverage the user' social circle to provide useful features. I think of it as Spotify connecting to your Facebook friends, or WhatsApp reading your phone's contact list.


Exposing the contact list:

1. **Removes friction** — products show names instead of raw addresses.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but not of all raw addresses and names, but very specific to the host list of contacts.

2. **Enables cross-product identity** — multiple products resolve the same contact within their respective contexts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously we state "cross-context access is a separate privilege". I think what we are trying to unlock is "per-product views of a shared local contact".

3. **Preserves user control** — the host gates access and filters responses to the requesting product's scope.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exclusive of this RFC. Too generic point.

4. **Supports contextual accounts** — a contact has different aliases and accounts per DotNS context, preserving unlinkability.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite generic too. I think what are trying to say is, this "lets users navigate the different aliases and accounts another user has across contexts."


## Detailed Design

### Data Model

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section presents the structs as if defining the address book from scratch, but each host already has a contact list. Today on desktop it's P2PPeer { type, accountId, name }; on mobile it's Chat.Contact { accountId, username, ownKeyId, pin, pushToken, peerPlatform, imageData, ... }. Neither matches the RFC's model, and chat features actively depend on the existing schemas. I don't think we should introduce a new shape in isolation.


```rust
type ContactContext = ProductAccountId; // (DotNsIdentifier, DerivationIndex)

struct ContextContactInfo {
alias: Option<Vec<u8>>,
account_id: Option<AccountId>
}

struct LocalContactInfo {
display_name: Option<str>
}

struct Contact {
local: LocalContactInfo,
entries: Map<ContactContext, ContextContactInfo>
}
```

`ContactContext` is a `ProductAccountId` (`DotNsIdentifier` + `DerivationIndex`) — the same tuple used for Ring VRF alias derivation. The host derives the `[u8; 32]` Ring VRF context by hashing this identifier internally.

`ContextContactInfo` fields are optional; either or both may be present.

### Access Tiers

#### Tier 1: Own-context (default)

The host filters `entries` to only the requesting product's `ProductAccountId`. `LocalContactInfo` is always included. This is safe because the product could already derive this information through its own alias system.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is safe because the product could already derive this information through its own alias system.

What do you mean by this? The product has access to the user alias, but not the aliases of other users.

Also, a good point that we side stepping is "How does a contact end up in the contact list to begin with?".


#### Tier 2: Cross-context (privileged)

Returns the full `entries` map. Required for host-privileged products that aggregate identities across contexts (e.g. Browse, profile, honour). The host MAY grant implicit tier 2 access to built-in host products that need it for their core function (e.g. a contact management UI).

### API

```rust
enum ContactsErr {
NotConnected,
Rejected,
Unknown(GenericErr)
}

fn host_contacts_get() -> Result<Vec<Contact>, ContactsErr>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this have an argument DotNS identifier. If the identifier matches the current app i.e. user is in host-playground.dot and the app calls host_contacts_get("host-playground") then it falls into tier 1 access, while if host_contacts_get("browse.dot") it falls into the tier 2 access?


fn host_contacts_subscribe(
callback: fn(Vec<Contact>)
) -> Result<Subscriber, ContactsErr>;
```

Both require authentication (RFC-0009). The host prompts for permission before returning. `host_contacts_subscribe` delivers the full filtered list on each callback; hosts MAY debounce.

This API returns only contacts the user has explicitly saved in their address book. It is not a global name resolution service — resolving arbitrary accounts to DotNS names is a separate concern (on-chain DotNS lookup).

### Permission Model

Extends `DevicePermission` from RFC-0002 with two new variants:

```rust
enum DevicePermission {
// ... existing variants ...
Contacts,
ContactsCrossContext
}
```

| Permission | Tier | Grants |
|-----------|------|--------|
| `Contacts` | 1 | Own-context entries + local info |
| `ContactsCrossContext` | 2 | Full entries across all contexts |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these permissions be added in RFC-002?


The tier 2 prompt SHOULD warn that the product can correlate contacts across contexts. `ContactsCrossContext` implies `Contacts`.

### Example

```
Product ("voting.dot", 0) calls host_contacts_get():

→ Host checks DevicePermission::Contacts grant
→ Host filters each contact's entries to key ("voting.dot", 0)
→ Returns:
[
Contact {
local: { display_name: "Alice" },
entries: { ("voting.dot", 0): { alias: 0xab.., account_id: 0x12.. } }
},
Contact {
local: { display_name: "Bob" },
entries: {} // Bob has no entry in ("voting.dot", 0) context
}
]
```

### Privacy-Preserving Display

The host can render a contact picker in a privileged overlay using full contact data, returning only the selected contact's own-context entry to the product. This lets users see rich details without the product receiving cross-context data. The overlay mechanism is host-specific and out of scope.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this would be an implementation detail related to how the permission modal would render in the UI, no?


## Drawbacks

- **Privacy surface.** Even tier 1 reveals the user's social graph. The permission prompt mitigates but does not eliminate this.
- **Full-list delivery.** No per-contact queries. The overlay pattern partially addresses this for picker UIs.
- **Read-only.** Products cannot add contacts. Deferred intentionally.

## Alternatives

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very hard to depict what these alternatives were.


### A: Freeform context keys

Loses alignment with Ring VRF contexts and makes scoping ambiguous.

### B: Per-contact lookup by alias

Requires knowing the alias upfront; does not support browsing.

### C: No context scoping

Breaks unlinkability — any product could correlate aliases across all contexts.

## Unresolved Questions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add "how does a contact end up in the address book to begin with?".


1. **Honour.** Needs a protected path so UAs can display honour without exposing the alias to the product. Whether honour is per-product or universal (or both) needs design. Likely a separate RFC.
2. **Common triage contexts.** Should well-known contexts (profile, honour) have a lighter permission model?
3. **Contact mutation.** Write access deferred to a follow-up RFC.
4. **Filtered subscriptions.** Should tier 2 `host_contacts_subscribe` accept a context filter?
5. **Overlay specification.** The exact overlay mechanism needs its own spec.
6. **Pagination.** May be needed for large contact lists.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would Pagination be a problem in terms of query size?

1 change: 1 addition & 0 deletions docs/rfcs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ created: 2026-03-13
| 0008 | [Statement Store Host API v0.2](0008-statement-store.md) | accepted | @johnthecat | [#118](https://github.com/paritytech/triangle-js-sdks/pull/118) | [v0.7 → Statement Store Subscribe API](../migration/v0.7.md#statement-store-subscribe-api-rfc-0008) |
| 0009 | [Unauthenticated Product Access](0009-unauthenticated-product-access.md) | accepted | @filvecchiato | [#128](https://github.com/paritytech/triangle-js-sdks/pull/128) | [v0.7 → Request Login](../migration/v0.7.md#new-host_request_login-rfc-0009) |
| 0010 | [Root account access Host API](0010-get-root-account.md) | accepted | @johnthecat | [#126](https://github.com/paritytech/triangle-js-sdks/pull/126) | [v0.7 → Root Account](../migration/v0.7.md#requesting-root-account-from-product) |
| 0011 | [Contacts API](0011-contacts-api.md) | draft | @filvecchiato | - | - |