368 lines
15 KiB
Markdown
368 lines
15 KiB
Markdown
|
|
---
|
||
|
|
title: "China-Friendly Wallet Login"
|
||
|
|
type: brainstorm
|
||
|
|
date: 2026-06-01
|
||
|
|
---
|
||
|
|
|
||
|
|
# China-Friendly Wallet Login
|
||
|
|
|
||
|
|
## Problem Statement
|
||
|
|
|
||
|
|
ARK Library needs wallet-based login so users can later access account-bound features such as favorites. The login must work for China-based users without requiring VPN access where possible. The goal is not to perform on-chain reads or transactions; it is only to verify wallet address ownership through message signing and bind that address to a backend session/JWT.
|
||
|
|
|
||
|
|
The practical problem is that mobile users may open the DApp in different environments: desktop browser with extension, wallet DApp browser, or a normal mobile browser. A normal mobile browser cannot directly talk to a wallet app unless there is a bridge such as WalletConnect/Reown, a wallet-specific SDK, or a wallet-specific callback/deep-link flow.
|
||
|
|
|
||
|
|
## Context
|
||
|
|
|
||
|
|
Existing backend wallet authentication is partially available:
|
||
|
|
|
||
|
|
- `POST /api/auth/wallet/nonce`
|
||
|
|
- `POST /api/auth/wallet/verify`
|
||
|
|
- `GET /api/auth/wallet/me`
|
||
|
|
|
||
|
|
Backend findings:
|
||
|
|
|
||
|
|
- Wallet nonce currently expires after 15 minutes.
|
||
|
|
- Wallet JWT currently lasts 30 days.
|
||
|
|
- There is no refresh-token mechanism.
|
||
|
|
- There is no user-bound favorites API yet.
|
||
|
|
- Existing `/api/resources/{id}/favorite` only changes global favorite count and does not bind favorites to a wallet address.
|
||
|
|
|
||
|
|
Frontend findings:
|
||
|
|
|
||
|
|
- There is currently no wallet login implementation.
|
||
|
|
- Favorites page is currently a placeholder.
|
||
|
|
- Wallet login should be designed separately from the full favorites feature.
|
||
|
|
|
||
|
|
Research findings:
|
||
|
|
|
||
|
|
- RainbowKit is a wallet UI layer. Its generic scan/login flow usually depends on WalletConnect/Reown.
|
||
|
|
- Reown/WalletConnect relay was previously found unstable for China access.
|
||
|
|
- RainbowKit can appear stable when users are actually connecting through injected providers (`window.ethereum`) in extensions or wallet DApp browsers; that stability does not prove WalletConnect/Reown scan stability.
|
||
|
|
- TokenPocket supports stable external-browser flows through `tpoutside://pull.activity` deep links and callback URLs.
|
||
|
|
- TokenPocket `tp-js-sdk` only works inside TokenPocket's DApp browser, so it is not the external-browser QR bridge by itself.
|
||
|
|
- imToken QR login generally uses WalletConnect-style bridging.
|
||
|
|
- MetaMask QR login can use WalletConnect/RainbowKit-style bridging or MetaMask SDK. For this project, MetaMask QR is handled through RainbowKit as a fallback rather than a China-stable primary path.
|
||
|
|
- OKX Connect SDK was considered but rejected because OKX Wallet is not a target wallet for this product.
|
||
|
|
|
||
|
|
## Chosen Approach
|
||
|
|
|
||
|
|
Use a hybrid wallet login approach:
|
||
|
|
|
||
|
|
1. **Stable primary path:** custom injected-provider login plus TokenPocket QR/callback login.
|
||
|
|
2. **Compatibility fallback:** RainbowKit/Reown QR login for MetaMask and imToken users who want scan-login from a separate device.
|
||
|
|
|
||
|
|
Supported wallets for this design:
|
||
|
|
|
||
|
|
1. TokenPocket
|
||
|
|
2. MetaMask
|
||
|
|
3. imToken
|
||
|
|
|
||
|
|
Supported login paths:
|
||
|
|
|
||
|
|
| Wallet | Injected / DApp browser login | Click / deep-link login | QR login back to current browser |
|
||
|
|
|---|---:|---:|---:|
|
||
|
|
| TokenPocket | Yes | Yes | Yes, via TokenPocket callback |
|
||
|
|
| MetaMask | Yes | Yes | Yes, via RainbowKit/Reown fallback |
|
||
|
|
| imToken | Yes | Yes | Yes, via RainbowKit/Reown fallback |
|
||
|
|
|
||
|
|
The UI must not imply that all QR methods are equally stable in China. It should distinguish:
|
||
|
|
|
||
|
|
- **TokenPocket QR login** — recommended China-stable QR path.
|
||
|
|
- **MetaMask / imToken QR login** — compatibility fallback powered by RainbowKit/Reown; may fail or be slow depending on network environment.
|
||
|
|
|
||
|
|
## Why This Approach
|
||
|
|
|
||
|
|
This approach balances China stability with the user's requirement that MetaMask and imToken also have QR login options.
|
||
|
|
|
||
|
|
Accepted trade-offs:
|
||
|
|
|
||
|
|
- TokenPocket gets the primary QR login because it provides a direct callback mechanism that avoids Reown/WalletConnect relay instability.
|
||
|
|
- MetaMask and imToken get QR login through RainbowKit/Reown, but this is explicitly treated as a fallback and not the recommended China-stable path.
|
||
|
|
- The frontend will include heavier wallet dependencies for the fallback path: RainbowKit, wagmi, viem, and Reown/WalletConnect configuration.
|
||
|
|
- A WalletConnect/Reown project ID is required through environment configuration.
|
||
|
|
- Full favorites behavior is out of scope for this spec and should be designed separately.
|
||
|
|
|
||
|
|
Rejected alternatives:
|
||
|
|
|
||
|
|
1. **RainbowKit/Reown for all wallets including TokenPocket**
|
||
|
|
- Rejected because it would make the China-stable TokenPocket flow depend on the relay that was already found unstable.
|
||
|
|
|
||
|
|
2. **TokenPocket-only QR login**
|
||
|
|
- Rejected because the desired product behavior now includes MetaMask and imToken QR login, even if those QR paths are less reliable in China.
|
||
|
|
|
||
|
|
3. **OKX Connect SDK**
|
||
|
|
- Rejected because OKX Wallet is not a target wallet for the current product requirement.
|
||
|
|
|
||
|
|
4. **MetaMask SDK separate integration**
|
||
|
|
- Rejected for now because RainbowKit/Reown gives a broader compatibility fallback for both MetaMask and imToken with one integration.
|
||
|
|
|
||
|
|
5. **Favorites in the same plan**
|
||
|
|
- Rejected because wallet login and user-bound favorites are separate subsystems. Favorites needs its own backend endpoints and product decisions.
|
||
|
|
|
||
|
|
## Design
|
||
|
|
|
||
|
|
### Architecture
|
||
|
|
|
||
|
|
The login feature should be split into small units:
|
||
|
|
|
||
|
|
1. **Wallet auth API client**
|
||
|
|
- Requests nonce.
|
||
|
|
- Verifies signed messages.
|
||
|
|
- Fetches current wallet session.
|
||
|
|
- Stores and clears JWT.
|
||
|
|
|
||
|
|
2. **Wallet session provider**
|
||
|
|
- Owns login state: loading, logged out, logged in, error.
|
||
|
|
- Exposes wallet address, shortened address, token status, login actions, logout action.
|
||
|
|
- Restores session through `/api/auth/wallet/me` when a token exists.
|
||
|
|
|
||
|
|
3. **Injected provider adapter**
|
||
|
|
- Uses `window.ethereum` when available.
|
||
|
|
- Requests accounts.
|
||
|
|
- Signs nonce via `personal_sign`.
|
||
|
|
- Sends address/signature to backend verify endpoint.
|
||
|
|
- Covers desktop extensions and wallet DApp browsers.
|
||
|
|
|
||
|
|
4. **TokenPocket QR adapter**
|
||
|
|
- Creates a server-recognized `actionId` / login request.
|
||
|
|
- Gets or constructs a TokenPocket `tpoutside://pull.activity` login/sign deep link.
|
||
|
|
- Displays it as QR code.
|
||
|
|
- Frontend polls backend for callback result.
|
||
|
|
- Once backend has the address/signature result, frontend finalizes login through normal wallet verification.
|
||
|
|
- This is the recommended QR path for China users.
|
||
|
|
|
||
|
|
5. **RainbowKit QR fallback adapter**
|
||
|
|
- Configures RainbowKit/wagmi/WalletConnect for MetaMask and imToken QR login.
|
||
|
|
- Uses `VITE_WALLETCONNECT_PROJECT_ID` for the Reown project ID.
|
||
|
|
- After wallet connection, requests a backend nonce and asks the connected wallet to sign the nonce.
|
||
|
|
- Sends address/signature to `/api/auth/wallet/verify` and stores the returned JWT.
|
||
|
|
- UI copy must label this as a fallback that may be unstable on some China networks.
|
||
|
|
|
||
|
|
6. **Wallet deep-link helper**
|
||
|
|
- Provides buttons for TokenPocket, MetaMask, and imToken.
|
||
|
|
- Opens the current URL in the selected wallet's DApp browser when no injected provider is available or when the user chooses the DApp-browser path.
|
||
|
|
|
||
|
|
7. **Wallet login modal**
|
||
|
|
- Shows wallet options.
|
||
|
|
- Shows TokenPocket QR as the recommended scan option.
|
||
|
|
- Shows MetaMask/imToken QR via RainbowKit as an alternate scan option.
|
||
|
|
- Shows device-specific copy:
|
||
|
|
- Desktop TokenPocket QR: "Use TokenPocket on your phone to scan this QR code."
|
||
|
|
- Mobile TokenPocket QR: "Use TokenPocket on another device to scan this QR code."
|
||
|
|
- RainbowKit fallback: "MetaMask / imToken QR uses WalletConnect/Reown and may be unstable on some networks. If it fails, open this site inside your wallet app."
|
||
|
|
|
||
|
|
### User Flow: Injected Login
|
||
|
|
|
||
|
|
1. User clicks Connect Wallet.
|
||
|
|
2. Frontend detects `window.ethereum`.
|
||
|
|
3. Frontend requests wallet accounts.
|
||
|
|
4. Frontend requests nonce from backend.
|
||
|
|
5. User signs nonce through wallet.
|
||
|
|
6. Frontend sends address and signature to backend verify endpoint.
|
||
|
|
7. Backend verifies signature and returns JWT.
|
||
|
|
8. Frontend stores JWT and updates UI to shortened address.
|
||
|
|
|
||
|
|
### User Flow: TokenPocket QR Login
|
||
|
|
|
||
|
|
1. User opens login modal and chooses TokenPocket QR.
|
||
|
|
2. Frontend creates a TokenPocket QR login request with a unique `actionId`.
|
||
|
|
3. Frontend displays a QR code for TokenPocket.
|
||
|
|
4. User scans QR with TokenPocket on another device.
|
||
|
|
5. TokenPocket asks user to sign the login message.
|
||
|
|
6. TokenPocket sends result to backend callback URL.
|
||
|
|
7. Frontend polls backend for `actionId` result.
|
||
|
|
8. Frontend receives address/signature result and completes verify flow.
|
||
|
|
9. Frontend stores JWT and updates UI.
|
||
|
|
|
||
|
|
### User Flow: MetaMask / imToken QR Fallback
|
||
|
|
|
||
|
|
1. User chooses MetaMask/imToken QR login.
|
||
|
|
2. Frontend opens RainbowKit's connection flow with WalletConnect/Reown configured.
|
||
|
|
3. User scans the QR using MetaMask or imToken.
|
||
|
|
4. WalletConnect/Reown establishes the session.
|
||
|
|
5. Frontend requests a backend nonce.
|
||
|
|
6. User signs the nonce through the connected wallet.
|
||
|
|
7. Frontend sends address/signature to `/api/auth/wallet/verify`.
|
||
|
|
8. Frontend stores JWT and updates UI.
|
||
|
|
9. If connection fails or times out, UI recommends TokenPocket QR or opening the site inside the wallet DApp browser.
|
||
|
|
|
||
|
|
### User Flow: MetaMask / imToken Deep Link
|
||
|
|
|
||
|
|
1. User clicks MetaMask or imToken button.
|
||
|
|
2. If injected provider exists, use injected login.
|
||
|
|
3. If no injected provider exists, open current site URL in the selected wallet's DApp browser using that wallet's deep-link/universal-link format.
|
||
|
|
4. The login completes inside the wallet DApp browser using injected login.
|
||
|
|
|
||
|
|
### Logged-In UI
|
||
|
|
|
||
|
|
- Desktop/header should show shortened address such as `0x12...ab34`.
|
||
|
|
- Clicking the address opens a small menu with Disconnect.
|
||
|
|
- Disconnect clears the local JWT/session and returns UI to logged-out state.
|
||
|
|
- No ENS lookup is required.
|
||
|
|
- No remote avatar lookup is required.
|
||
|
|
|
||
|
|
### Backend API Contract
|
||
|
|
|
||
|
|
Existing wallet auth endpoints should remain the canonical verification path:
|
||
|
|
|
||
|
|
```http
|
||
|
|
POST /api/auth/wallet/nonce
|
||
|
|
POST /api/auth/wallet/verify
|
||
|
|
GET /api/auth/wallet/me
|
||
|
|
```
|
||
|
|
|
||
|
|
The frontend needs exact request/response contracts confirmed before implementation. Expected shape:
|
||
|
|
|
||
|
|
```http
|
||
|
|
POST /api/auth/wallet/nonce
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{ "address": "0x..." }
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{ "nonce": "message to sign" }
|
||
|
|
```
|
||
|
|
|
||
|
|
```http
|
||
|
|
POST /api/auth/wallet/verify
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{ "address": "0x...", "signature": "0x..." }
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{ "token": "jwt", "address": "0x..." }
|
||
|
|
```
|
||
|
|
|
||
|
|
For TokenPocket QR login, backend needs additional endpoints or equivalent behavior:
|
||
|
|
|
||
|
|
```http
|
||
|
|
POST /api/auth/wallet/tp-login-request
|
||
|
|
```
|
||
|
|
|
||
|
|
Creates a short-lived login request and returns data needed to render the QR.
|
||
|
|
|
||
|
|
Expected output:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"actionId": "unique-id",
|
||
|
|
"message": "message to sign",
|
||
|
|
"qrUrl": "tpoutside://pull.activity?param=...",
|
||
|
|
"expiresAt": "ISO timestamp"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```http
|
||
|
|
POST /api/auth/wallet/tp-callback
|
||
|
|
```
|
||
|
|
|
||
|
|
Called by TokenPocket after user signs. Backend validates the callback payload shape, stores the result by `actionId`, and expires it quickly.
|
||
|
|
|
||
|
|
```http
|
||
|
|
GET /api/auth/wallet/tp-result?actionId=...
|
||
|
|
```
|
||
|
|
|
||
|
|
Frontend polls this endpoint until result is pending, completed, expired, or failed.
|
||
|
|
|
||
|
|
Expected states:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{ "status": "pending" }
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"status": "completed",
|
||
|
|
"address": "0x...",
|
||
|
|
"signature": "0x..."
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{ "status": "expired" }
|
||
|
|
```
|
||
|
|
|
||
|
|
The final JWT should still come from `/api/auth/wallet/verify` so all wallet-login paths share one verification endpoint.
|
||
|
|
|
||
|
|
RainbowKit/Reown QR fallback does not require new backend endpoints beyond the canonical nonce/verify/me endpoints, but it does require frontend environment configuration:
|
||
|
|
|
||
|
|
```env
|
||
|
|
VITE_WALLETCONNECT_PROJECT_ID=...
|
||
|
|
```
|
||
|
|
|
||
|
|
### Error Handling
|
||
|
|
|
||
|
|
- No wallet detected: show wallet choices, TokenPocket QR login, and RainbowKit QR fallback.
|
||
|
|
- User rejects signature: show a clear retryable error.
|
||
|
|
- Nonce expired: request a fresh nonce and retry.
|
||
|
|
- TokenPocket QR expired: generate a new QR.
|
||
|
|
- TokenPocket callback never arrives: show timeout and retry option.
|
||
|
|
- RainbowKit/Reown connection fails: explain that scan login may be blocked or slow on this network; recommend TokenPocket QR or wallet DApp browser.
|
||
|
|
- Invalid signature: show login failed and do not store token.
|
||
|
|
- `/me` fails with expired/invalid token: clear token and return to logged-out state.
|
||
|
|
|
||
|
|
### Testing
|
||
|
|
|
||
|
|
Frontend testing should cover:
|
||
|
|
|
||
|
|
- Session provider restores logged-in state when `/me` succeeds.
|
||
|
|
- Session provider clears state when `/me` fails.
|
||
|
|
- Injected adapter signs and verifies through mocked provider/API.
|
||
|
|
- TokenPocket QR polling handles pending, completed, expired, failed, and timeout states.
|
||
|
|
- RainbowKit fallback handles connected, rejected, timeout/failure, and signed-message states.
|
||
|
|
- Login modal copy correctly distinguishes TokenPocket QR from RainbowKit/Reown fallback QR.
|
||
|
|
- Logout clears token and resets UI.
|
||
|
|
|
||
|
|
Backend testing should cover:
|
||
|
|
|
||
|
|
- TokenPocket login request creates short-lived action IDs.
|
||
|
|
- Callback stores exactly one completed result per action ID.
|
||
|
|
- Expired action IDs cannot be completed.
|
||
|
|
- Polling endpoint returns correct states.
|
||
|
|
- Verify endpoint still validates signatures and returns JWT.
|
||
|
|
|
||
|
|
## Implementation Checklist
|
||
|
|
|
||
|
|
- [ ] Confirm exact existing wallet auth request/response shapes with backend.
|
||
|
|
- [ ] Confirm TokenPocket callback payload fields from official docs or a sandbox callback test.
|
||
|
|
- [ ] Confirm WalletConnect/Reown project ID ownership and add `VITE_WALLETCONNECT_PROJECT_ID` to env docs.
|
||
|
|
- [ ] Add backend TokenPocket login request endpoint.
|
||
|
|
- [ ] Add backend TokenPocket callback endpoint.
|
||
|
|
- [ ] Add backend TokenPocket polling/result endpoint.
|
||
|
|
- [ ] Add frontend install plan for RainbowKit, wagmi, viem, and required query provider dependency.
|
||
|
|
- [ ] Add frontend wallet auth API client and token storage helpers.
|
||
|
|
- [ ] Add frontend wallet session provider/hook.
|
||
|
|
- [ ] Add injected provider login adapter.
|
||
|
|
- [ ] Add TokenPocket QR login adapter and polling flow.
|
||
|
|
- [ ] Add RainbowKit/Reown QR fallback for MetaMask and imToken.
|
||
|
|
- [ ] Add wallet deep-link helpers for TokenPocket, MetaMask, and imToken.
|
||
|
|
- [ ] Add wallet login modal and header logged-in state UI.
|
||
|
|
- [ ] Wire logout to clear token and session state.
|
||
|
|
- [ ] Add frontend tests for session, injected login, TP QR polling, RainbowKit fallback, and logout.
|
||
|
|
- [ ] Add backend tests for TokenPocket request/callback/result behavior.
|
||
|
|
- [ ] Document the backend API contract for Louis/backend implementation.
|
||
|
|
|
||
|
|
## Open Questions
|
||
|
|
|
||
|
|
1. What exact message format should users sign? It should include domain, wallet address, nonce, issued-at time, and purpose such as "Sign in to ARK Library".
|
||
|
|
2. Should JWT remain 30 days, or should backend add refresh tokens later?
|
||
|
|
3. What exact TokenPocket callback payload will be received for EVM personal-sign login?
|
||
|
|
4. Which public icon URL should TokenPocket QR metadata use for ARK Library?
|
||
|
|
5. Should login UI appear only in the header and favorites flow, or also in mobile menu?
|
||
|
|
6. Should favorites trigger wallet login immediately when clicked, or should that be decided in the separate favorites design?
|
||
|
|
7. Which Reown/WalletConnect project ID should production use, and who owns that Reown project?
|
||
|
|
8. Should RainbowKit fallback be hidden or visually de-emphasized for China users, or simply shown with warning copy?
|
||
|
|
|
||
|
|
## Out of Scope
|
||
|
|
|
||
|
|
- Full user-bound favorites implementation.
|
||
|
|
- Favorites page real list UI.
|
||
|
|
- Favorites database schema.
|
||
|
|
- OKX Connect SDK.
|
||
|
|
- MetaMask SDK separate integration.
|
||
|
|
- ENS names, ENS avatars, or chain data reads.
|
||
|
|
- On-chain transactions.
|