AddressLink
AddressLink is Superhero's identity contract. It maps an address to a map(provider, value),
holding one value per provider (an X handle, a nostr public key, a preferred AENS name). It
also keeps a reverse index for resolving a value back to an address and a per-address nonce
sequence for signed writes.
A link can be owned by a bare account, by an AENS name (a principal), or by a DID with a controlling address. Each family has its own entrypoints, its own nonce space and its own resolver.
State
record state =
{ providers : map(string, address) // provider id -> owning account
, links : map(address, map(string, string)) // address -> provider -> value
, reverse : map(string, map(string, address)) // provider -> value -> address
, nonce_seq : map(address, int) }Entrypoints
| Entrypoint | Signature |
|---|---|
register_provider | (provider: string) => unit |
link | (addr: address, provider: string, value: string, nonce: int, sig: signature) => unit |
unlink | (addr: address, provider: string, nonce: int, sig: signature) => unit |
link_principal | (principal: string, signer: address, provider: string, value: string, nonce: int, sig: signature) => unit |
unlink_principal | (principal: string, signer: address, provider: string, nonce: int, sig: signature) => unit |
link_did | (did: string, controller: address, provider: string, value: string, nonce: int, sig: signature) => unit |
unlink_did | (did: string, controller: address, provider: string, nonce: int, sig: signature) => unit |
get_nonce | (addr: address) => int |
get_nonce_principal | (principal: string, signer: address) => int |
get_nonce_did | (did: string, controller: address) => int |
get_links | (addr: address) => map(string, string) |
get_link | (addr: address, provider: string) => option(string) |
get_provider_owner | (provider: string) => option(address) |
resolve | (provider: string, value: string) => option(address) |
resolve_signer | (principal: string) => option(address) |
resolve_did_controller | (did: string) => option(address) |
Providers
A provider id is a namespace owned by an account. register_provider(provider) claims it,
and only the owner may write links under it. Any other caller aborts with
NOT_PROVIDER_OWNER. Provider ids are at most ten lowercase a-z characters
(/^[a-z]{1,10}$/), which is why the AENS provider is spelled prefaens.
The providers Superhero operates:
| Provider id | Links |
|---|---|
x | an X handle |
nostr | a nostr public key |
site | a website |
bio | a bio |
prefaens | a preferred AENS name |
The signed message
Every write carries an explicit nonce and a signature from the address being linked. Read the
current nonce with get_nonce(addr) (or get_nonce_principal / get_nonce_did), then sign a
plain-text, colon-separated message with the verb first and the nonce last:
link:<address>:<provider>:<value>:<nonce>
unlink:<address>:<provider>:<nonce>The principal variants substitute the AENS name for the address in the same layout. The
signature is 64 bytes, submitted as a 128-character hex string. A stale or reused nonce aborts
with INVALID_NONCE; a signature that does not verify aborts with INVALID_SIGNATURE.
Linking through the Superhero API
Because Superhero owns the providers it operates, the user signs and the backend submits. The REST surface is two calls per operation:
| Step | Endpoint | What it does |
|---|---|---|
| 1 | POST /address-links/{provider}/claim | verifies you control the external account, reads get_nonce, and returns the exact message to sign |
| 2 | POST /address-links/{provider}/submit | takes value, nonce and signature, and relays the link call |
| 1 | POST /address-links/{provider}/unclaim | returns the unlink message and nonce |
| 2 | POST /address-links/{provider}/unclaim/submit | relays the unlink call |
See Profile API for the request and response shapes.
Resolving links
| Read | Returns |
|---|---|
get_links(addr) | every link for an address, as provider -> value |
get_link(addr, provider) | one value, or None |
resolve(provider, value) | the address that owns value under provider, or None |
resolve_signer(principal) | the account that signs for an AENS name |
resolve_did_controller(did) | the controlling address of a DID |
get_provider_owner(provider) | the account that owns a provider namespace |
The Superhero API resolves an account's public name in the order: prefaens link, AENS name
from the middleware, legacy username, raw address. The X handle is returned separately as
x_username.
Abort strings
| Abort | Meaning |
|---|---|
INVALID_SIGNATURE | the signature does not verify against the message and address |
INVALID_NONCE | the nonce does not match get_nonce |
ALREADY_CLAIMED | this provider is already linked to a different value for this address |
NOT_LINKED | no link exists for this provider |
NO_LINKS | the address has no links at all |
NOT_PROVIDER_OWNER | the caller does not own the provider namespace |
PRINCIPAL_NOT_FOUND | the AENS name does not resolve |
PRINCIPAL_MISMATCH | only the name owner can link or unlink |
INVALID_PRINCIPAL | the principal string is malformed |
Events
datatype event =
ProviderRegistered(address, string) // provider owner, provider id
| Link(address, string)
| Unlink(address, string)
| PrincipalLink(address, string)
| PrincipalUnlink(address, string)Every event carries an address topic and a string in data. On the link events the
data string is provider:value, colon-joined. Topic hashes are in the
event catalogue.
Related
- Superhero ID: the identity system from a user's point of view
- Chain names: AENS names, which are verifiable without this contract
- X verification: the claim-sign-submit flow in the app
- AddressLink: the identity-section view of linking
- Profile API: the endpoints in the table above
- Events: the
AddressLinktopic hashes in the cross-contract catalogue