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>
11 KiB
title, tags, created, status, priority
| title | tags | created | status | priority | |||||
|---|---|---|---|---|---|---|---|---|---|
| PRD — TON Wallet Ownership Proof |
|
2026-05-24 | backend-implemented | 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:
{
"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 byuserId. - 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:
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):
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
// models/User.ts — inside profile
walletProofVerified?: boolean;
walletProofTimestamp?: Date;
4. Dependencies to add
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):
// 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
// 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
// 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
tonProofobject setswalletProofVerified = false(legacy format-only path).
Frontend
connectWalletis called withtonProof: 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/challengeimplemented inuserController.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.3added to backendpackage.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
POST /api/user/wallet-address/ton-proof/challengereturns a valid nonce object.PATCH /api/user/wallet-addresswith a real TonConnect proof from Telegram Wallet succeeds and setswalletProofVerified: true.- A replay of the same proof is rejected.
- An expired nonce is rejected.
- 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.
- Backend tests cover all rejection cases with mocked proofs.
@ton/tonversion is pinned inpackage.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