Given a wallet address and chain, discover all DIDs, SBTs, attestations, and credentials attached to it. Pluggable providers, pluggable priorities.
@attestto/wallet-identity-resolver is the identity middleware that runs after wallet connection. WalletConnect, Phantom, and Wagmi give you an address and a signer — they stop there. @attestto/wallet-identity-resolver takes that address and resolves the full identity graph: DIDs (did:sns, did:ens, did:web), SNS/ENS domains, KYC credentials, Civic passes, Soulbound Tokens, vLEI attestations. You pick which providers to trust and in what priority order.
Part of the Attestto open identity ecosystem.
flowchart TB
A["resolveIdentities({ address, chain, providers })"] --> B{For each provider}
B --> C["SNS Provider"]
B --> D["Attestto Creds Provider"]
B --> E["Civic Provider"]
B --> F["caip10() fallback"]
C -->|"did:sns:alice.sol"| G["ResolvedIdentity[]"]
D -->|"KYC VC + SBT"| G
E -->|"Civic Pass token"| G
F -->|"caip10:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:..."| G
G --> H["Unified results — ordered by provider priority"]
style A fill:#1a1a2e,stroke:#7c3aed,color:#e0e0e0
style G fill:#1a1a2e,stroke:#10b981,color:#e0e0e0
style H fill:#1a1a2e,stroke:#10b981,color:#e0e0e0
The consumer (your dApp) decides which identity types to accept and in what priority — no hardcoded assumptions, no hardcoded endpoints.
- Node.js 16+
- A wallet address to resolve
- At least one identity provider configured
# Core engine (required)
npm install @attestto/wallet-identity-resolver
# Provider plugins (install only what you need)
npm install @attestto/wir-sns # SNS .sol domains → did:sns
npm install @attestto/wir-ens # ENS .eth domains → did:ens
npm install @attestto/wir-attestto-creds # Attestto KYC VCs + SBTs
npm install @attestto/wir-civic # Civic Pass gateway tokens
npm install @attestto/wir-sas # Solana Attestation Serviceimport { resolveIdentities } from '@attestto/wallet-identity-resolver'
import { sns } from '@attestto/wir-sns'
import { attesttoCreds } from '@attestto/wir-attestto-creds'
import { civic } from '@attestto/wir-civic'
import { caip10 } from '@attestto/wallet-identity-resolver'
const identities = await resolveIdentities({
chain: 'solana',
address: 'ATTEstto1234567890abcdef...',
providers: [
attesttoCreds({ programId: '...', rpcUrl: '...' }),
sns({ apiUrl: '...', resolverUrl: '...' }),
civic({ apiUrl: '...' }),
caip10(), // Fallback — always resolves
],
})
// identities: ResolvedIdentity[]
// Each entry: { provider, did, label, type, meta }The engine tries each provider in order. If one finds results, you get those (plus caip10 fallback). If none find anything, you still get a caip10 result that's valid for any address on any chain.
packages/
core/ → @attestto/wallet-identity-resolver (engine + caip10 fallback)
sns/ → @attestto/wir-sns (SNS .sol domains → did:sns)
ens/ → @attestto/wir-ens (ENS .eth domains → did:ens)
attestto-creds/ → @attestto/wir-attestto-creds (KYC, SBTs, VCs)
civic/ → @attestto/wir-civic (Civic Pass gateway tokens)
sas/ → @attestto/wir-sas (Solana Attestation Service)
WalletConnect, Dynamic, and Wagmi are crypto wallet connectors. They connect MetaMask, Phantom, and Ledger to dApps for transaction signing. Once connected, they give you an address and a signer. That's where they stop.
This package is the identity layer that comes after. It's DNS for compliance — given a wallet address, it resolves the full identity graph attached to it: DIDs, domains, KYC credentials, institutional attestations, soulbound tokens.
A traditional bank operating under SWIFT/ISO 20022 cannot interact with a DeFi protocol using WalletConnect alone. Before allowing the transaction, the protocol needs to resolve the bank's did:web or vLEI credential to verify institutional identity and compliance status. No existing wallet connector does this.
| Step | Layer | Role | Output |
| 1 | WalletConnect / Phantom | 🔌 | Address + signer |
| 2 | identity-resolver | 🔍 | DIDs, KYC status, vLEI, SBTs, domains |
| 3 | id-wallet-adapter | 🛡️ | VP request + cryptographic verification |
| Existing connectors handle step 1. Steps 2–3 are the identity middleware that crypto wallets are missing. | |||
| WalletConnect / Dynamic / Wagmi | identity-resolver | |
|---|---|---|
| Purpose | Connect wallet, sign transactions | Resolve identities from an address |
| Input | User action (QR scan, click) | Wallet address (string) |
| Output | Signer + address | DID Documents, linked identities (alsoKnownAs), KYC status, institutional credentials |
| When | Before any interaction | After wallet connection |
| Compliance | None — no identity awareness | FATF Travel Rule, eIDAS 2.0, GLEIF vLEI ready |
- WalletConnect → connect Solana/Ethereum wallet → get address
- identity-resolver → resolve that address → find SNS domain, Attestto credentials, Civic pass, vLEI attestation
- id-wallet-adapter → discover credential wallet extensions → request Verifiable Presentation → verify cryptographically
Step 1 uses existing connectors. Steps 2–3 are what we built — the identity middleware that MetaMask, Phantom, and every crypto wallet are currently missing. By following W3C CHAPI and DIDComm v2 standards, this stack is already compatible with the regulatory direction of eIDAS 2.0 (EU), FATF Travel Rule, and jurisdictional digital identity wallet mandates.
Several projects resolve partial identity data from addresses. None offer a pluggable, multi-chain resolution engine.
| Project | What it does | What it doesn't do |
|---|---|---|
| @talismn/on-chain-id | Resolves ENS (Ethereum) and Polkadot on-chain identity for addresses | No Solana. No pluggable provider architecture. No DID resolution, KYC, vLEI, or SBT support. |
| @onchain-id/identity-sdk | ERC734/735 identity smart contracts (Ethereum) | Ethereum-only. Contract-level SDK, not a resolver. Last published 2+ years ago. |
| DID-native VC toolkits | Issue and verify VCs. Resolve DIDs via did:web, did:key, etc. |
Resolve a single DID — do not discover all identities attached to an address. No provider plugin system. |
| Credo-ts | Full DIDComm + OID4VP agent framework with DID resolution | Agent framework, not an address-to-identity resolver. Requires running a full agent. |
| @digitalbazaar/vc | W3C VC issuance and verification (JSON-LD) | VC operations only. No address-to-identity discovery. |
Where identity-resolver fits: Given a wallet address, no existing package answers "what DIDs, KYC credentials, vLEI attestations, SBTs, and domains are attached to this address?" across multiple chains. identity-resolver is the only pluggable engine where you pick your providers, set their priority, and get a unified ResolvedIdentity[] back — with per-provider timeouts, cancellation, and zero hardcoded endpoints.
Core function that discovers all identities attached to a wallet address.
interface ResolveOptions {
chain: Chain // 'solana', 'ethereum', or custom
address: string // Wallet address / public key
providers: IdentityProvider[] // Ordered list — defines priority
rpcUrl?: string // Global RPC override (passed to providers)
timeoutMs?: number // Per-provider timeout (default 5000ms)
stopOnFirst?: boolean // Stop after first provider returns results
signal?: AbortSignal // Cancellation
}Result from a single provider:
interface ResolvedIdentity {
provider: string // Which provider found this
did: string | null // Resolved DID, if applicable
label: string // Human-readable label
type: IdentityType // 'domain' | 'sbt' | 'attestation' | 'credential' | 'did' | 'score'
meta: Record<string, unknown> // Provider-specific metadata
}Implement this to create a custom identity provider:
interface IdentityProvider {
name: string
chains: string[] // Which chains you support
resolve(ctx: ResolveContext): Promise<ResolvedIdentity[]>
}
interface ResolveContext {
chain: string
address: string
rpcUrl?: string
signal?: AbortSignal
}Providers are tried in order. Order = priority. The engine guarantees:
- Try each provider in the order you pass them
- Collect all results
- If nothing found, return
caip10()fallback (valid for any address on any chain) - If
stopOnFirst: true, stop after the first provider that returns results
Related repos in the Attestto ecosystem:
| Package | Purpose | Repo |
|---|---|---|
| @attestto/id-wallet-adapter | Discover credential wallet extensions, request and verify Verifiable Presentations | GitHub |
| @attestto/verify | Web Components for wallet discovery, signing, and VP verification | GitHub |
| @attestto/vc-sdk | Issue and verify W3C Verifiable Credentials | GitHub |
| did-sns-spec | did:sns DID method spec — Solana domain to DID resolution |
GitHub |
| vLEI-Solana-Bridge | Write and verify vLEI attestations from GLEIF on Solana | GitHub |
This repo ships a llms.txt context file — a machine-readable summary of the API, data structures, and integration patterns designed to be read by AI coding assistants.
Use the attestto-dev-mcp server to give your LLM active access to the ecosystem:
cd ../attestto-dev-mcp
npm install && npm run buildThen add it to your Claude / Cursor / Windsurf config and ask:
"Explore the Attestto ecosystem and scaffold me an on-chain identity resolver"
We recommend Claude Pro (5× usage vs free) or higher. Long context and strong TypeScript reasoning handle this codebase well. The MCP server works with any LLM that supports tool use.
Quick start: Ask your LLM to read
llms.txtin this repo, then describe what you want to build. It will find the right archetype, generate boilerplate, and walk you through the first run.
The DID Landscape Explorer uses this package in its self-assessment wizard. When a user connects a Web3 wallet, the explorer resolves all identities attached to their address and lets them pick which DID to sign with.
import { resolveIdentities, caip10 } from '@attestto/wallet-identity-resolver'
import { ens } from '@attestto/wir-ens'
const identities = await resolveIdentities({
chain: 'ethereum',
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
providers: [
ens({ resolverUrl: 'https://api.yourapp.com/resolver' }),
caip10(),
],
})No hardcoded endpoints. Every provider requires explicit URLs from the consumer. No provider ever makes a network call to an endpoint you didn't configure. See SECURITY.md for the full security model.
Recommended architecture:
flowchart LR
A[Browser<br>no keys, no direct RPC] --> B[Your backend proxy<br>holds API keys, validates origin]
B --> C[Solana RPC]
B --> D[Bonfida]
B --> E[UniResolver]
B --> F[Civic]
| Package | Chain | What it resolves | Required options |
|---|---|---|---|
@attestto/wallet-identity-resolver |
any | Core engine + caip10() fallback |
— |
@attestto/wir-sns |
Solana | SNS .sol domains → did:sns |
apiUrl, resolverUrl |
@attestto/wir-ens |
Ethereum | ENS .eth domains → did:ens |
resolverUrl |
@attestto/wir-attestto-creds |
Solana | Attestto KYC, identity SBTs, VCs | programId, rpcUrl |
@attestto/wir-civic |
Solana | Civic Pass gateway tokens | apiUrl |
@attestto/wir-sas |
Solana | Solana Attestation Service | programId, rpcUrl |
Any identity source can become a provider. Follow these steps:
Create an interface for the configuration your provider needs. All network endpoints must be required (no hardcoded URLs).
interface MyProviderOptions {
/** Your API endpoint — consumer must provide this (required) */
apiUrl: string
/** Optional filter */
category?: string
}Export a function that takes your options and returns an IdentityProvider. This is the pattern every built-in provider follows.
import type { IdentityProvider } from 'identity-resolver'
export function myProvider(options: MyProviderOptions): IdentityProvider {
return {
name: 'my-provider', // Unique name — shows up in ResolvedIdentity.provider
chains: ['solana'], // Which chains you support (use ['*'] for all chains)
resolve: async (ctx) => {
// ... (Step 3)
},
}
}The engine calls resolve(ctx) with the wallet address and chain. Your job:
- Call your data source (API, RPC, on-chain program)
- Map results to
ResolvedIdentity[] - Return
[]if nothing found — never throw
import type { IdentityProvider, ResolveContext, ResolvedIdentity } from 'identity-resolver'
export function myProvider(options: MyProviderOptions): IdentityProvider {
return {
name: 'my-provider',
chains: ['solana'],
async resolve(ctx: ResolveContext): Promise<ResolvedIdentity[]> {
// ctx.chain — 'solana', 'ethereum', etc.
// ctx.address — the wallet public key / address
// ctx.rpcUrl — optional RPC override from the consumer
// ctx.signal — AbortSignal for cancellation (pass to fetch!)
try {
const res = await fetch(
`${options.apiUrl}/lookup/${ctx.address}`,
{ signal: ctx.signal },
)
if (!res.ok) return []
const data = await res.json() as { items: Array<{ name: string; did?: string }> }
if (!data.items?.length) return []
return data.items.map((item) => ({
provider: 'my-provider', // Must match the name above
did: item.did ?? null, // DID string, or null if not applicable
label: item.name, // Human-readable label for display
type: 'credential' as const, // 'domain' | 'sbt' | 'attestation' | 'credential' | 'did' | 'score'
meta: { raw: item }, // Anything extra — consumers access via meta
}))
} catch {
return [] // Never throw — return empty on failure
}
},
}
}Pass your provider to resolveIdentities alongside any others. Order = priority.
import { resolveIdentities, caip10 } from 'identity-resolver'
import { myProvider } from './my-provider'
const identities = await resolveIdentities({
chain: 'solana',
address: pubkey,
providers: [
myProvider({ apiUrl: 'https://my-backend.com/api/lookup' }),
caip10(), // always-available fallback
],
})To share your provider as an npm package:
- Name it
@yourorg/wir-<name>(convention, not required) - Add
identity-resolveras a peer dependency for type compatibility - Export your factory function + options interface
{
"name": "@yourorg/wir-my-provider",
"peerDependencies": {
"identity-resolver": "^0.1.0"
}
}| Rule | Why |
|---|---|
| No hardcoded URLs | Consumer controls infrastructure, keys, CORS |
Never throw from resolve() |
One broken provider must not crash the chain |
Return [] on failure |
Empty = nothing found, engine moves on |
Pass ctx.signal to fetch |
Supports cancellation and timeouts |
Use meta for extras |
Don't extend ResolvedIdentity — put provider-specific data in meta |
| One provider = one source | Keep providers focused (SNS, Civic, etc.) |
pnpm install
pnpm run build # Build all packages
pnpm run lint # Type-check all packagesSee CONTRIBUTING.md.
MIT