# Wallet sign-in

Let people sign in and sign up by signing a message with an Ethereum or Solana wallet in their browser.

For an app whose customers already live in a crypto wallet, a password is one more thing to make up. Wallet sign-in lets them sign in by signing a message with the wallet in their browser: Sign-In with Ethereum (EIP-4361), or its Solana counterpart. A wallet nobody has seen before signs up with a name and an email, as a new phone number does. It all runs on your hosted sign-in page.

Step by step, with screenshots: [Set up wallet sign-in](/sign-in-setup/wallets).

> **Note:**
  Wallet sign-in needs a plan that includes it. Without one, the console shows
  the method locked and adding it is refused with a `402`.


## Setting it up

1. Open **User Management → Access → Authentication → Methods**, choose **Add auth method**, then **Wallet**.
2. Choose the **Ethereum networks** a wallet may sign in from: Ethereum, Polygon, Base, Arbitrum One and Optimism are listed by name, and **Other chain IDs** takes any other EIP-155 chain ID. Only Ethereum mainnet (chain ID 1) is allowed until you change it.
3. Turn **Solana** on if Solana wallets may sign in. It is off by default.
4. Tick the method on your client in the **Settings** tab.

A method has to allow at least one Ethereum network or Solana, or no wallet could sign in, and saving it is refused.

## Which wallets work

**Injected wallets with plain keys.** For Ethereum, the EIP-1193 provider a browser wallet injects as `window.ethereum`; for Solana, a Phantom-style `window.solana`. The address is the key that signs.

Two kinds of wallet do not work yet, on purpose:

- **WalletConnect**, the QR code that connects a phone wallet to a desktop page. It needs a project ID registered by each app owner.
- **Smart-contract wallets** (ERC-1271). Checking their signatures needs a call to the chain, and BuildBase checks every signature offline.

## What people see

The sign-in page offers the wallet, and asks it to sign a message the page builds from a one-time nonce. Nothing is sent to the chain and nothing costs gas. A wallet already linked to an account signs straight in.

- **A new wallet** is asked for a name and an email after signing. The email starts unverified: a verification code goes to it, as after any sign-up, so wallet sign-up needs a connected [email sender](/email/overview). An address that already has an account here is refused, with the same sentence whether it is taken or not allowed. Linking the wallet there would hand the account to whoever holds the wallet; its owner can connect the wallet from their own Security screen instead.
- **Sign-up rules apply.** [Closed sign-up](/authentication/overview#closing-public-sign-up), [who can sign up](/authentication/signup-access), disposable addresses and your plan's end-user limit all hold for a new wallet.
- **The second step still applies.** A wallet sign-in is one factor, `swk` in the token's `amr`, so a person with an authenticator app is asked for its code after signing, as after a password.

## What is checked

A signed message is believed only when all of these hold:

- it names your sign-in page's domain and address, so a message signed for another site is refused
- its nonce is one BuildBase issued for this sign-in, less than 5 minutes ago, and it is spent on first use
- its network is one the method allows. Solana has no network setting: a message naming mainnet, devnet or testnet is accepted
- its times hold: issued recently, not expired, not dated in the future
- the signature belongs to the address in the message, recovered with secp256k1 for Ethereum and checked with ed25519 for Solana

A wallet is then a connected account: provider `ethereum` with the EIP-55 checksum address, or `solana` with the base58 address.

## Connecting a wallet from your app

A signed-in person can add a wallet to the account they already have. While your client offers wallet sign-in, the **Connected accounts** section of the SDK's Security screen shows **Connect an Ethereum wallet** and **Connect a Solana wallet**. Connected wallets are listed with a short address, count as a way in, and emit `user.account_linked`. Hide only the connect buttons with `ui.settings.security.wallets: false`.

Without the screen, `connectWallet` runs the whole exchange: it gets a nonce, asks an Ethereum wallet on another network to switch to an allowed one, has the wallet sign, and links the result.

```ts
import { connectWallet, WalletFlowError } from '@buildbase/sdk/react';

try {
  const account = await connectWallet('ethereum', { api }); // api: a WorkspaceApi
  console.log(account.detail); // the short address
} catch (error) {
  // no_wallet, rejected, wallet_busy, wrong_network, no_account, wallet_failed
  if (error instanceof WalletFlowError) console.warn(error.code);
  else throw error;
}
```

**Connect from your app's own page.** A wallet shows the person which site is asking for the signature, and warns when the message names a different one. So a link's message names your app, not the sign-in page, and the request has to come from one of your allowed app origins: the origin of one of your clients' redirect URLs. From anywhere else it is refused with `403 origin_not_allowed`.

| Refusal                    | When                                                                               |
| -------------------------- | ---------------------------------------------------------------------------------- |
| `409` `link_conflict`      | The wallet is connected to another account here                                    |
| `409` `guest_account`      | A [guest](/authentication/guests) creates an account before connecting a wallet    |
| `401` `nonce_expired`      | The nonce lapsed or was used. Start again. The SDK does not sign anyone out for it |
| `400` `chain_not_allowed`  | The method does not take that kind of wallet, or that network                      |
| `400` `invalid_message`    | The message was not made for this page, or its times are off                       |
| `400` `invalid_signature`  | The signature is not the address's                                                 |
| `403` `origin_not_allowed` | The request did not come from one of your allowed app origins                      |
| `403` `method_unavailable` | Your client offers no wallet sign-in                                               |

## If you call the auth API yourself

The hosted page does this for you. Its three steps, with the sign-in page's state header:

| Route                               | Body                                            | Answers                                                          |
| ----------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------- |
| `POST /api/v1/auth/wallet/nonce`    | `{ chain }`, `ethereum` or `solana`             | `{ nonce, domain, uri, issuedAt, chainIds }`                     |
| `POST /api/v1/auth/wallet/verify`   | `{ chain, message, signature }`                 | A sign-in, or `{ signupRequired, signupToken }` for a new wallet |
| `POST /api/v1/auth/wallet/register` | `{ signupToken, name, email, turnstileToken? }` | A sign-in, as phone sign-up answers                              |

`/verify` refuses with `400` `invalid_signature`, `invalid_message` or `chain_not_allowed`, and `401` `nonce_expired`. `/register` answers as [phone sign-up](/authentication/phone#if-you-call-the-auth-api-yourself) does, from `409 email_unavailable` to `401 signup_expired`.

## Next steps

- [Connected Accounts](/authentication/connected-accounts) - wallets beside social sign-ins, and removing one.
- [Phone Sign-in](/authentication/phone) - the other sign-up that asks for an email after the fact.
- [Two-step sign-in](/authentication/two-step) - the second step after a wallet signature.
