Files
nick-doc/07 - Development/PRD - TON Wallet Ownership Proof.md
Siavash Sameni 873a57874e Update docs for TON proof backend, Mini App fixes, and EVM wallet stub
PRD - TON Wallet Ownership Proof.md:
- Status updated from ready-to-implement -> backend-implemented.
- Added Implementation Status section documenting what is complete
  (challenge endpoint, tonProofService.ts, User model fields, 15 tests)
  and what remains (frontend proof wiring, verified badge).
- Acceptance criteria updated: backend items checked, frontend pending.

Handoff - Telegram Mini App Debug - 2026-05-24.md:
- New Implemented sections for session 3:
  - TON Wallet Ownership Proof backend (full detail of tonProofService,
    userController changes, User model fields, 15 unit tests).
  - Telegram Mini App shell dir="ltr" + smart displayName fallback.
  - Socket status suppressed on /telegram paths.
  - EVM WalletConnect stub card (disabled until project ID configured).
- Known Issues: TON proof updated to "frontend wiring pending";
  EVM WalletConnect section added with activation steps.
- Current Git State updated to Session 3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:21:29 +04:00

312 lines
11 KiB
Markdown

---
title: PRD — TON Wallet Ownership Proof
tags: [prd, ton, wallet, security, telegram]
created: 2026-05-24
status: backend-implemented
priority: high
---
# PRD — TON Wallet Ownership Proof
## Background
TON wallet connect was shipped as a first-pass integration: the backend validates address format and stores it, but does not require the user to prove they control the private key behind the address. Any user can submit any valid-looking TON address and it will be saved to their profile.
This matters for:
- **Seller payout routing** — if a seller's wallet address was spoofed, funds go to the wrong address.
- **On-chain proof of identity** — downstream features (NFT-gated access, on-chain reputation) require a real ownership proof.
- **Compliance** — KYC/AML flows that tie a real-world identity to a wallet need proof, not self-report.
Until proof is implemented, saved TON wallet addresses are self-reported metadata only and must not be used for automatic payout routing.
---
## Goal
Replace the format-only TON wallet save with a full TonConnect ownership proof flow:
- Backend issues a challenge nonce scoped to the user session.
- Frontend passes the challenge to TonConnect UI, which asks the user to sign it.
- Backend verifies the returned proof against the wallet's public key.
- On success, stores `walletProofVerified: true`, proof timestamp, and wallet provider.
---
## Non-Goals
- On-chain transaction signing or payment initiation (separate feature).
- Requiring proof for EVM wallets (already covered by `ethers.verifyMessage`).
- TON DNS or NFT resolution.
- Mandatory wallet for all users (wallet remains optional profile metadata).
---
## User Stories
| As a… | I want to… | So that… |
|-------|-----------|----------|
| Seller | Connect my Telegram Wallet with proof | Payments route to the right address |
| Buyer | Know my counterparty's wallet is verified | I trust the seller's payout address |
| Admin | See `walletProofVerified` on user profiles | I can audit wallet verification status |
| Developer | Have a clear nonce/challenge API | I can build additional proof-gated features |
---
## Flow
```
Frontend Backend TonConnect
| | |
|-- POST /wallet-address/ton-proof/challenge --> |
| |-- generate nonce |
| |-- store nonce (TTL 5 min) |
|<-- { payload, domain, timestamp } -- |
| | |
|-- tonConnectUI.connectWallet({ tonProof: payload }) ---------->|
| | |-- user signs proof
|<-- proof object (wallet, stateInit, signature) ---------------|
| | |
|-- PATCH /wallet-address { walletType:'ton', tonProof } --> |
| |-- verify proof |
| | - reconstruct message |
| | - verify against pubkey |
| | - check domain + nonce |
| |-- store wallet + proof meta |
|<-- { walletProofVerified: true } -- |
```
---
## Backend Changes
### 1. New endpoint: `POST /api/user/wallet-address/ton-proof/challenge`
**Auth:** Bearer JWT required.
**Request body:** none.
**Response:**
```json
{
"payload": "aGV4LWVuY29kZWQtbm9uY2U...",
"domain": "amn.gg",
"timestamp": 1716560000
}
```
**Implementation:**
- Generate 32-byte random nonce, hex-encode.
- Store in a TTL map (or Redis `SET NX EX 300`) keyed by `userId`.
- Return `{ payload: nonce, domain: "amn.gg", timestamp: now }`.
- One active challenge per user — issuing a new one invalidates the old.
---
### 2. Updated: `PATCH /api/user/wallet-address`
**When `walletType = 'ton'` and a `tonProof` object is present:**
Accept additional body field:
```ts
tonProof?: {
timestamp: number;
domain: { lengthBytes: number; value: string };
signature: string; // base64
payload: string; // the nonce from challenge
stateInit?: string; // base64, for wallets not yet deployed
}
```
**Verification logic (using `@ton/ton` and `@ton/crypto`):**
```ts
import { Address, Cell, beginCell, contractAddress } from '@ton/ton';
import nacl from 'tweetnacl';
// 1. Validate nonce matches stored challenge for this user
// 2. Validate timestamp is within 5-minute window
// 3. Validate domain === 'amn.gg'
// 4. Reconstruct the signed message:
const message = Buffer.concat([
Buffer.from('ton-proof-item-v2/'),
Buffer.from(new Uint32Array([domain.length]).buffer),
Buffer.from(domain, 'utf8'),
Buffer.from(new Uint64Array([timestamp]).buffer),
Buffer.from(payload, 'hex'),
]);
const hash = Buffer.from(sha256(Buffer.concat([
Buffer.from([0xff, 0xff]),
Buffer.from('ton-connect'),
sha256(message),
])));
// 5. Recover public key from stateInit if wallet not deployed,
// or fetch from blockchain (tonweb/lite-client) if deployed.
// 6. Verify: nacl.sign.detached.verify(hash, sig, pubkey)
// 7. Verify: Address.parse(walletAddress) matches derived address
```
**On success:**
- Store wallet fields: `profile.walletAddress`, `profile.walletType = 'ton'`, `profile.walletProvider`.
- Set new fields: `profile.walletProofVerified = true`, `profile.walletProofTimestamp = new Date()`.
- Clear the stored nonce.
**On failure:**
- Return `400 INVALID_TON_PROOF`.
- Do not update wallet fields.
- Do not clear the nonce (allow retry within TTL).
---
### 3. User model additions
```ts
// models/User.ts — inside profile
walletProofVerified?: boolean;
walletProofTimestamp?: Date;
```
---
### 4. Dependencies to add
```bash
yarn add @ton/ton @ton/crypto tweetnacl
```
`@ton/ton` is already a known dependency (the `@ton/core` frontend package is related). The backend needs `@ton/ton` for address parsing and cell operations, and `tweetnacl` for ed25519 verification.
---
## Frontend Changes
### 1. Fetch challenge before connecting
In `TonWalletProvider` (or the connect handler in `account-wallet-connection.tsx`):
```ts
// Before calling tonConnectUI.connectWallet()
const { payload } = await api.post('/api/user/wallet-address/ton-proof/challenge');
// Pass proof request to TonConnect
const proof: ConnectAdditionalRequest = {
tonProof: payload,
};
await tonConnectUI.connectWallet(proof);
```
### 2. Extract and send proof after connection
```ts
// After connection, wallet.connectItems.tonProof is populated
const wallet = tonConnectUI.wallet;
if (!wallet?.connectItems?.tonProof || 'error' in wallet.connectItems.tonProof) {
throw new Error('TON proof not returned by wallet');
}
await api.patch('/api/user/wallet-address', {
walletAddress: wallet.account.address,
walletType: 'ton',
walletProvider: wallet.device.appName,
tonProof: wallet.connectItems.tonProof.proof,
});
```
### 3. Show proof badge on wallet page
When `profile.walletProofVerified = true`, show a "Verified" badge next to the wallet address. When false (or absent), show "Unverified" with a "Verify ownership" button.
---
## Data Model
```ts
// Addition to profile sub-schema
walletProofVerified?: boolean; // true after successful TonConnect proof
walletProofTimestamp?: Date; // when proof was last verified
```
Existing fields retain their meaning:
- `walletAddress` — the TON address (friendly format)
- `walletType``'ton'` | `'evm'`
- `walletProvider` — e.g. `'Telegram Wallet'`, `'Tonkeeper'`
---
## API Reference
| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| POST | `/api/user/wallet-address/ton-proof/challenge` | JWT | Get nonce for proof |
| PATCH | `/api/user/wallet-address` | JWT | Save wallet + verify proof |
---
## Tests
### Backend
- Challenge endpoint returns `{ payload, domain, timestamp }`.
- Challenge is bound to userId (different users get different nonces).
- Challenge expires: re-issuing invalidates previous.
- Wallet save with valid proof sets `walletProofVerified = true`.
- Wallet save with expired nonce returns 400.
- Wallet save with wrong domain returns 400.
- Wallet save with bad signature returns 400.
- Wallet save without `tonProof` object sets `walletProofVerified = false` (legacy format-only path).
### Frontend
- `connectWallet` is called with `tonProof: payload`.
- If proof is absent in response, shows error and does not call save endpoint.
- If backend returns 400, shows "Wallet verification failed" toast.
- Verified wallet shows badge; unverified shows "Verify" prompt.
---
## Migration
Existing users with a saved TON wallet address will have `walletProofVerified` absent (undefined / falsy). They should be prompted to re-verify when they next visit the wallet page. No automatic migration needed — the address is retained, only proof status changes.
---
## Implementation Status
### Backend — Complete (2026-05-24)
- `POST /api/user/wallet-address/ton-proof/challenge` implemented in `userController.ts`.
- Full ed25519 verification in `src/services/user/tonProofService.ts`:
- In-memory TTL challenge store (5-minute TTL per userId).
- Nonce cleared on success only (allows retry on failure).
- Verification: nonce match → expiry → domain → timestamp ±300s → stateInit hash matches address → extract pubkey → reconstruct signed message → `nacl.sign.detached.verify`.
- Public key extraction handles standard wallet v3/v4 data layout (skip 64 bits seqno+subwallet, read 32-byte pubkey).
- User model fields added: `profile.walletProofVerified`, `profile.walletProofTimestamp`.
- Route registered: `POST /api/user/wallet-address/ton-proof/challenge`.
- `@ton/ton@16.2.4`, `@ton/crypto@3.3.0`, `tweetnacl@1.0.3` added to backend `package.json`.
- 15 backend unit tests in `__tests__/ton-proof.test.ts` — all passing.
### Frontend — Pending
- Challenge fetch + `setConnectRequestParameters` + proof extraction not yet wired.
- Verified/unverified badge not yet rendered.
- EVM wallet stub card added (see below).
---
## Acceptance Criteria
- [x] `POST /api/user/wallet-address/ton-proof/challenge` returns a valid nonce object.
- [ ] `PATCH /api/user/wallet-address` with a real TonConnect proof from Telegram Wallet succeeds and sets `walletProofVerified: true`.
- [x] A replay of the same proof is rejected.
- [x] An expired nonce is rejected.
- [x] A proof with a mismatched domain is rejected.
- [ ] The wallet page shows "Verified" badge when `walletProofVerified = true`.
- [ ] The wallet page shows "Verify ownership" CTA when wallet is saved but unverified.
- [x] Backend tests cover all rejection cases with mocked proofs.
- [x] `@ton/ton` version is pinned in `package.json` (TON SDK breaks semver occasionally).
---
## Related
- [[Handoff - Telegram Mini App Debug - 2026-05-24]]
- [[Security Audit - 2026-05-24]] — TON wallet is currently self-reported metadata
- [[Data Model Overview]]
- TON Connect docs: https://docs.ton.org/develop/dapps/ton-connect/sign