# Wallet login (`user-sig`)

A human signs in with their wallet and your app receives a stable,
app-scoped **pseudonym** — not their address. There is no browser redirect,
no issuer, and no session-granting server in between: the wallet signs, the
SDK mints a **deed**, and your app verifies it directly against the
on-chain registry. If you haven't already, read [Concepts](concepts.md) for
the mental model.

## TypeScript: `signInWithDeed`

`@grantor/sdk` exports `signInWithDeed`, which composes the wallet-signing
steps ([The flow](#the-flow), below) and the origin-provenance check into one call:

```ts
import { signInWithDeed } from "@grantor/sdk";

const deed = await signInWithDeed({
  signMessage,      // wraps the wallet's personal_sign
  tenantId,
  audience,
  origin,           // the origin YOU are actually running on
  challenge,        // from your app's own /challenge
  exp,
  vouch,            // the RP's published origin_vouch
  chainId,          // registry defaults to Registry.canonical(), resolved against this chain id
  chainReader,
});
```

`registry` defaults to `Registry.canonical()` — the SDK pins it for you; pass
`registry: Registry.devnet(addr)` for local development or
`Registry.dedicated(addr, license)` for a [licensed enterprise
registry](enterprise-registry.md). The old `chainRegistry` option (a plain
address) is deleted — passing it throws a `TypeError` naming the
replacement, never a silent fall-through to the canonical default.

Python, Go and Rust expose the same steps (`deriveRootSeed`/`deriveAppKey`/
`mintUserDeed`, in their language-specific casing) as separate calls rather
than one wrapper. The Rust snippet in [Sovereign tier § User
login](../sovereign-tier.md#user-login-user-sig) is the source of truth;
`sdk/README.md` maps the equivalent calls per language.

## The flow

1. **Your app issues a challenge** — `GET /challenge` on your own
   [deed guard](verify-tokens.md).
2. **The wallet signs.** The SDK has the wallet sign a human-readable
   message naming the origin (twice, compared for determinism) to derive a
   `root_seed`, then derives a per-`(tenant, origin)` app key from it and
   signs the deed with that key. One wallet popup per origin, not one for
   the whole web.
3. **Your app verifies** the deed with `DeedGuard`/`DeedVerifier` and mints
   its own session, however it already does. See [Verify a
   deed](verify-tokens.md).

The full walkthrough — the exact derivation
(`derive_root_seed` → `derive_app_key` → `mint_user_deed`), what is and is
not in the key-derivation scope (`tenant` + `origin`, deliberately not
`aud`), and what a `sub` looks like across audiences/origins/tenants — is
documented once, in [Sovereign tier § User login
(`user-sig`)](../sovereign-tier.md#user-login-user-sig). Read that rather
than a second copy here.

## Any wallet, any chain

`user-sig` is wallet-**chain**-agnostic: the wallet only ever signs the
origin-binding message (`user_root_binding(origin)`), and the verifier never
sees which chain the wallet belongs to — only the secp256k1 app key
`deriveAppKey` derives from whatever signature comes back. An EVM wallet's
`personal_sign`, or a Solana / Aptos / Sui / Near wallet's `signMessage` — any
wallet that can produce a signature over that one message — mints the same
shape of deed and yields the same stable `(tenant, origin)` pseudonym
`user-sig` gives every other wallet.

```rust
use grantor_sdk_core::usersig::{derive_root_seed, derive_app_key, mint_user_deed};

// A non-EVM wallet's `signMessage` in place of `personal_sign` — the SDK
// does not care which. `derive_root_seed` signs the message TWICE and
// compares the results, so the closure just needs to return the wallet's
// raw signature bytes; nothing here assumes an EVM signature's shape.
let seed = derive_root_seed(origin, |msg| wallet.sign_message(msg))?;
let app_key = derive_app_key(&seed, tenant_id, origin)?;
let token = mint_user_deed(&app_key, tenant_id, audience, origin, &challenge, exp)?;
```

This is the same [flow](#the-flow) and the same mint API the EVM path uses —
there is no separate non-EVM entry point. Pass the wallet's `signMessage`
output where the EVM snippet passes `personal_sign`'s, and everything
downstream (`deriveAppKey`, `mintUserDeed`, verification) runs unchanged.
TypeScript, Python and Go expose the same `deriveRootSeed`/`deriveAppKey`/
`mintUserDeed` calls, in their own casing.

### Determinism is required

The wallet must sign **deterministically** — the same message must always
produce the same signature bytes. EVM `personal_sign` already is (RFC 6979);
a non-EVM wallet must use RFC 8032 **pure** ed25519 signing, not a
randomized ("hedged") variant. `deriveRootSeed` signs the binding message
twice and compares the results before deriving anything from it, so a
non-deterministic wallet is refused loudly — `SdkError::Crypto("wallet
signing is non-deterministic; unsupported")` (or the equivalent typed error
in your language) — rather than silently minting a new pseudonym on every
login.

## Origin provenance: required before signing

`signInWithDeed` (and `authenticate()`, the equivalent agent-side helper)
refuses to sign **before** it signs anything — not after — if the origin
cannot show a tenant admin's on-chain vouch that it speaks for
`(tenant, audience)`. This is what stops a hostile origin that republishes
a victim tenant's `tenant`/`audience` from harvesting a pseudonym by simply
asking a holder to sign in. `chainReader` is REQUIRED —
omitting it is a fail-closed error, not a skipped check. See [Sovereign
tier § Origin provenance](../sovereign-tier.md#origin-provenance) and [§
Constructing a `chainReader`](../sovereign-tier.md#constructing-a-chainreader).

## What this proves — and what it does not

`user-sig` proves *"I control a key"*, not *"I control a wallet"*.
Permissionless login is open by definition — anyone can generate a key. The
wallet's role is **portability of identity across devices**, not
gatekeeping. There is no on-chain user revocation, because there is no
on-chain user state to revoke; an RP that wants to ban someone bans the
`sub` on its own side.

`personal_sign` also means the wallet cannot tell who is asking: a hostile
page can request the *victim's* origin-scoped message, and the only control
left is a human reading a prompt that names an origin they are not on. See
[Sovereign tier § Why the seed is
origin-scoped](../sovereign-tier.md#why-the-seed-is-origin-scoped) for the full
residual-risk statement — do not describe this mode as a hard
phishing-resistance guarantee.

If you can require a passkey instead of a wallet, [Passkey
login](user-passkey.md) (`user-passkey`) closes exactly this residual via
browser-enforced origin binding. It is additive, not a replacement — this
page and `user-sig` remain the compatibility path for a caller who only has
a wallet.

## See also

- [Verify a deed](verify-tokens.md) — the relying-party side.
- [Agent tokens](agent-tokens.md) — the `agent-zk` deed, for enrolled agents.
- [Sovereign tier](../sovereign-tier.md) — the full reference.
- [Errors](errors.md) — every error code a relying party branches on.
