Smart Contracts
AddressLink

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

EntrypointSignature
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 idLinks
xan X handle
nostra nostr public key
sitea website
bioa bio
prefaensa 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:

StepEndpointWhat it does
1POST /address-links/{provider}/claimverifies you control the external account, reads get_nonce, and returns the exact message to sign
2POST /address-links/{provider}/submittakes value, nonce and signature, and relays the link call
1POST /address-links/{provider}/unclaimreturns the unlink message and nonce
2POST /address-links/{provider}/unclaim/submitrelays the unlink call

See Profile API for the request and response shapes.

Resolving links

ReadReturns
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

AbortMeaning
INVALID_SIGNATUREthe signature does not verify against the message and address
INVALID_NONCEthe nonce does not match get_nonce
ALREADY_CLAIMEDthis provider is already linked to a different value for this address
NOT_LINKEDno link exists for this provider
NO_LINKSthe address has no links at all
NOT_PROVIDER_OWNERthe caller does not own the provider namespace
PRINCIPAL_NOT_FOUNDthe AENS name does not resolve
PRINCIPAL_MISMATCHonly the name owner can link or unlink
INVALID_PRINCIPALthe 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 AddressLink topic hashes in the cross-contract catalogue