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>
This commit is contained in:
311
07 - Development/PRD - TON Wallet Ownership Proof.md
Normal file
311
07 - Development/PRD - TON Wallet Ownership Proof.md
Normal file
@@ -0,0 +1,311 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user