diff --git a/.gitignore b/.gitignore
index 871e9b7c0..925097fe9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+*aptos_examples*
+*APTOS*.md
# Specific branches ignores
*APTOS*.md
*_TO_PORT.md
@@ -119,6 +121,8 @@ architecture
blocked_ips.json
SMART_CONTRACTS_*.md
.gitbook*
+TG_IDENTITY_PLAN.md
+
# Aptos stuff
aptoswares/build
build/aptoswares
diff --git a/TG_INTEGRATION.md b/TG_INTEGRATION.md
new file mode 100644
index 000000000..a3030a898
--- /dev/null
+++ b/TG_INTEGRATION.md
@@ -0,0 +1,486 @@
+# Telegram Identity Integration Guide
+## For dApp and Bot Development Teams
+
+This document outlines how the **dApp** and **Bot** teams should work together to implement seamless Telegram identity verification using the Demos node APIs.
+
+## π **Security Update: Anti-Replay Protection**
+**NEW**: The system now includes enhanced security features to prevent replay attacks:
+- **Challenge Hash Embedding**: SHA256 hash of challenges embedded in transactions
+- **Validation Mode Security**: On-chain validation requires matching challenge hash
+- **Replay Attack Prevention**: Old attestations cannot be reused even with valid bot signatures
+
+## π― **Goal: Seamless User Experience**
+
+Instead of users manually copying/pasting messages, we want:
+1. **User clicks "Link Telegram" on dApp**
+2. **Automatic coordination between dApp and bot**
+3. **User signs transaction to complete** β
+
+---
+
+## ποΈ **Architecture Overview**
+
+```
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+β dApp β β Bot β β Node β
+β β β β β β
+β 1. Generate βββββΆβ β β β
+β Challenge β β β β β
+β β β 2. Listen for β β β
+β 3. Show Login β β User Auth β β β
+β Button β β β β β
+β β β 4. Create βββββΆβ 5. Verify & β
+β β β Attestation β β Create TX β
+β β β β β β
+β 7. Show TX ββββββ 6. Return TX ββββββ β
+β to Sign β β to dApp β β β
+β β β β β β
+β 8. Submit βββββββββββββββββββββββββββΆβ 9. Process β
+β Signed TX β β β β Identity β
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+```
+
+---
+
+## π **Option 1: Telegram Login Widget (Recommended)**
+
+**Most Seamless Approach** - Use official Telegram Login Widget
+
+### **dApp Implementation**:
+
+```html
+
+
+```
+
+### **Flow**:
+1. **User clicks Telegram Login Widget**
+2. **Telegram authenticates user** (official OAuth-like flow)
+3. **Telegram redirects to dApp** with auth data
+4. **dApp receives**: `{ id, first_name, username, photo_url, auth_date, hash }`
+5. **dApp generates challenge** via node API
+6. **dApp signs challenge** with user's wallet
+7. **dApp sends to bot** via Telegram Bot API (server-to-server)
+8. **Bot creates attestation** and returns unsigned transaction
+9. **dApp shows transaction** for user to sign
+10. **User signs and submits** β
+
+### **Advantages**:
+- β
**Official Telegram integration**
+- β
**No manual copy/paste**
+- β
**Seamless UX**
+- β
**Secure authentication**
+- β
**Works on all devices**
+
+---
+
+## π€ **Option 2: Bot API Direct Integration**
+
+**Alternative Approach** - Direct bot communication
+
+### **Flow**:
+1. **dApp generates unique session ID**
+2. **dApp shows**: "Click here to verify with @DemosBot"
+3. **Deep link**: `https://t.me/DemosBot?start=session_12345`
+4. **Bot receives session ID** from deep link
+5. **Bot calls dApp API**: "User started session_12345"
+6. **dApp sends challenge** to bot for that session
+7. **Bot gets user to sign** and creates attestation
+8. **Bot returns unsigned transaction** to dApp
+9. **dApp shows transaction** for user to sign
+
+### **Advantages**:
+- β
**No OAuth complexity**
+- β
**Direct bot control**
+- β
**Custom flow possible**
+
+---
+
+## π **Recommended Implementation: Option 1**
+
+### **dApp Team Tasks**:
+
+#### **1. Frontend Integration**
+```typescript
+// telegram-auth.ts
+interface TelegramAuthData {
+ id: number
+ first_name: string
+ username?: string
+ photo_url?: string
+ auth_date: number
+ hash: string
+}
+
+async function handleTelegramAuth(authData: TelegramAuthData) {
+ // 1. Verify Telegram auth hash (security)
+ if (!verifyTelegramAuth(authData)) {
+ throw new Error('Invalid Telegram authentication')
+ }
+
+ // 2. Generate challenge via node
+ const challengeResponse = await fetch('/api/tg-challenge', {
+ method: 'POST',
+ body: JSON.stringify({ demos_address: userWalletAddress })
+ })
+ const { challenge } = await challengeResponse.json()
+
+ // 3. Sign challenge with user's wallet
+ const signedChallenge = await wallet.signMessage(challenge)
+
+ // 4. Send to bot via Bot API (server-to-server)
+ const botResponse = await fetch('/api/telegram-bot-notify', {
+ method: 'POST',
+ body: JSON.stringify({
+ telegramUser: authData,
+ signedChallenge,
+ sessionId: generateSessionId()
+ })
+ })
+
+ // 5. Bot will process and return unsigned transaction
+ const { unsignedTransaction } = await botResponse.json()
+
+ // 6. Show transaction to user for signing
+ showTransactionModal(unsignedTransaction)
+}
+```
+
+#### **2. Bot Communication Endpoint**
+```typescript
+// pages/api/telegram-bot-notify.ts
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { telegramUser, signedChallenge, sessionId } = req.body
+
+ // Send to bot via Telegram Bot API
+ const botResponse = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ chat_id: telegramUser.id,
+ text: `Verification request received. Processing...`
+ })
+ })
+
+ // Create attestation via bot
+ const attestation = await createBotAttestation(telegramUser, signedChallenge)
+
+ // Get unsigned transaction from node
+ const nodeResponse = await fetch('https://node/api/tg-verify', {
+ method: 'POST',
+ body: JSON.stringify(attestation)
+ })
+
+ const { unsignedTransaction } = await nodeResponse.json()
+
+ res.json({ unsignedTransaction })
+}
+```
+
+### **Bot Team Tasks**:
+
+#### **1. Bot Setup**
+```python
+# bot.py
+import telegram
+from telegram.ext import Application, CommandHandler, MessageHandler
+import requests
+import json
+
+# Bot token from @BotFather
+BOT_TOKEN = "your_bot_token"
+NODE_URL = "https://node.demos.network"
+GENESIS_PRIVATE_KEY = "your_genesis_private_key"
+GENESIS_ADDRESS = "your_genesis_address"
+
+bot = telegram.Bot(token=BOT_TOKEN)
+```
+
+#### **2. Verification Handler**
+```python
+async def handle_verification_request(telegram_user_id, telegram_username, signed_challenge):
+ """
+ Handle verification request from dApp
+ Enhanced with anti-replay protection via challenge hash embedding
+ """
+ # Create attestation payload
+ attestation = {
+ 'telegram_id': str(telegram_user_id),
+ 'username': telegram_username or '',
+ 'signed_challenge': signed_challenge,
+ 'timestamp': int(time.time())
+ }
+
+ # Sign attestation with bot's genesis private key
+ attestation_json = json.dumps(attestation, sort_keys=True)
+ bot_signature = sign_message(attestation_json, GENESIS_PRIVATE_KEY)
+
+ # Submit to node (node will generate challenge hash automatically)
+ payload = {
+ **attestation,
+ 'bot_address': GENESIS_ADDRESS,
+ 'bot_signature': bot_signature
+ }
+
+ response = requests.post(f'{NODE_URL}/api/tg-verify', json=payload)
+
+ if response.status_code == 200:
+ data = response.json()
+ # Transaction now includes embedded challenge hash for replay protection
+ return data.get('unsignedTransaction')
+ else:
+ raise Exception(f"Node verification failed: {response.text}")
+```
+
+#### **3. Webhook/API Integration**
+```python
+# For dApp to communicate with bot
+@app.route('/webhook/verification', methods=['POST'])
+def handle_dapp_verification():
+ data = request.json
+ telegram_user = data['telegramUser']
+ signed_challenge = data['signedChallenge']
+
+ try:
+ unsigned_tx = await handle_verification_request(
+ telegram_user['id'],
+ telegram_user.get('username'),
+ signed_challenge
+ )
+
+ return jsonify({
+ 'success': True,
+ 'unsignedTransaction': unsigned_tx
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': str(e)
+ }), 400
+```
+
+---
+
+## π **Security Considerations**
+
+### **1. Telegram Auth Verification**
+```typescript
+// Verify Telegram authentication hash
+function verifyTelegramAuth(authData: TelegramAuthData, botToken: string): boolean {
+ const { hash, ...data } = authData
+
+ // Create data string
+ const dataCheckString = Object.keys(data)
+ .sort()
+ .map(key => `${key}=${data[key]}`)
+ .join('\n')
+
+ // Calculate expected hash
+ const secretKey = crypto.createHash('sha256').update(botToken).digest()
+ const expectedHash = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex')
+
+ return expectedHash === hash
+}
+```
+
+### **2. Anti-Replay Protection**
+```typescript
+// Challenge hash generation for replay protection
+function generateChallengeHash(challenge: string): string {
+ return crypto.createHash('sha256').update(challenge).digest('hex')
+}
+
+// Transaction payload must include challenge hash
+interface TelegramTransactionPayload {
+ context: 'telegram'
+ proof: string
+ username: string
+ userId: string
+ attestation_id: string // SHA256 hash of original challenge
+}
+```
+
+### **3. Session Management**
+- **Unique session IDs** for each verification attempt
+- **Expiration**: 15 minutes max per session
+- **Rate limiting**: Max 5 attempts per user per hour
+- **CSRF protection**: Validate origin and referrer
+
+### **4. Bot Security**
+- **Genesis address validation**: Bot must own genesis private key
+- **Signature verification**: All attestations cryptographically signed
+- **Challenge hash embedding**: All transactions include SHA256 challenge hash
+- **Replay attack prevention**: On-chain validation requires matching challenge hash
+- **IP allowlisting**: Only accept requests from known dApp servers
+- **Webhook authentication**: Verify requests from dApp
+
+---
+
+## π¨ **UX Flow Design**
+
+### **Ideal User Experience**:
+
+```
+1. User on dApp β Clicks "Connect Telegram" button
+2. Telegram Login Widget β Opens in popup/redirect
+3. User authorizes β Returns to dApp automatically
+4. dApp shows β "Signing challenge with wallet..."
+5. Wallet prompts β User signs challenge
+6. dApp shows β "Verifying with Telegram bot..."
+7. Process completes β "Sign transaction to finish linking"
+8. User signs TX β "β
Telegram linked successfully!"
+```
+
+### **Error Handling**:
+- **Telegram auth fails** β Clear error message + retry button
+- **Challenge signing fails** β Wallet-specific guidance
+- **Bot verification fails** β Contact support info
+- **Transaction fails** β Retry with gas adjustment
+
+---
+
+## π± **Mobile Considerations**
+
+### **Deep Links**:
+```typescript
+// For mobile apps
+const telegramDeepLink = `https://t.me/DemosBot?start=verify_${sessionId}`
+
+// Fallback for web
+const telegramWebLink = `https://web.telegram.org/k/#@DemosBot?start=verify_${sessionId}`
+```
+
+### **Responsive Design**:
+- **Mobile-first** Telegram login widget
+- **Touch-friendly** buttons and interfaces
+- **Proper viewport** meta tags
+- **App-like transitions** between steps
+
+---
+
+## π§ͺ **Testing Strategy**
+
+### **Integration Testing**:
+1. **dApp generates challenge** β Verify API response
+2. **Telegram auth simulation** β Mock auth data
+3. **Bot attestation creation** β Test signature validity
+4. **Node transaction creation** β Verify transaction structure
+5. **End-to-end flow** β Complete user journey
+
+### **Security Testing**:
+- **Invalid signatures** β Should be rejected
+- **Expired challenges** β Should fail gracefully
+- **Replay attacks** β Should be prevented
+- **Bot impersonation** β Should be blocked
+
+---
+
+## π **Deployment Coordination**
+
+### **Sequence**:
+1. **Node APIs** deployed and tested β
(Already done)
+2. **Bot** deployed with webhook endpoints
+3. **dApp** updated with Telegram integration
+4. **Integration testing** across all components
+5. **Production rollout** with monitoring
+
+### **Configuration**:
+```env
+# dApp .env
+TELEGRAM_BOT_USERNAME=DemosBot
+TELEGRAM_BOT_WEBHOOK_URL=https://bot.demos.network/webhook
+NODE_API_URL=https://node.demos.network
+
+# Bot .env
+BOT_TOKEN=your_telegram_bot_token
+GENESIS_PRIVATE_KEY=your_genesis_private_key
+GENESIS_ADDRESS=your_genesis_address
+WEBHOOK_SECRET=random_secret_for_dapp_communication
+DAPP_ALLOWED_ORIGINS=https://app.demos.network,https://demos.network
+```
+
+---
+
+## π **Success Metrics**
+
+### **Technical KPIs**:
+- **Completion rate** >85% (users who start finish successfully)
+- **Error rate** <5% (failed verifications)
+- **Response time** <3 seconds average
+- **Mobile compatibility** >95% success rate
+
+### **User Experience KPIs**:
+- **Time to complete** <60 seconds average
+- **User satisfaction** >4.5/5 rating
+- **Support tickets** <2% of attempts
+- **Retry rate** <10% of users
+
+---
+
+## π **Troubleshooting Guide**
+
+### **Common Issues**:
+
+| Issue | Cause | Solution |
+|-------|-------|----------|
+| "Invalid Telegram auth" | Hash verification failed | Check bot token, verify hash calculation |
+| "Challenge expired" | >15 minutes elapsed | Generate fresh challenge |
+| "Challenge hash mismatch" | Replay attack detected | Ensure transaction includes correct challenge hash |
+| "Unauthorized bot" | Bot not using genesis key | Verify genesis private key |
+| "Transaction failed" | Invalid transaction format | Check transaction structure and attestation_id |
+| "Wallet won't sign" | Wrong network/format | Verify wallet connection |
+
+### **Debug Tools**:
+```typescript
+// Add to dApp for debugging
+const DEBUG_MODE = process.env.NODE_ENV === 'development'
+
+if (DEBUG_MODE) {
+ console.log('Challenge:', challenge)
+ console.log('Challenge Hash:', generateChallengeHash(challenge))
+ console.log('Signed challenge:', signedChallenge)
+ console.log('Unsigned transaction:', unsignedTransaction)
+ console.log('Transaction attestation_id:', unsignedTransaction.content.data[1].attestation_id)
+}
+}
+```
+
+---
+
+## π **Support Contacts**
+
+- **Node API Issues**: Backend team
+- **Bot Integration**: Bot development team
+- **dApp Integration**: Frontend team
+- **User Experience**: Product team
+
+---
+
+## π― **Next Steps**
+
+### **Immediate (Week 1)**:
+1. **Bot team**: Set up Telegram bot with webhook endpoints
+2. **dApp team**: Implement Telegram Login Widget
+3. **Both teams**: Create communication protocol
+
+### **Integration (Week 2)**:
+1. **Test** individual components
+2. **Connect** dApp β Bot communication
+3. **End-to-end** testing
+
+### **Launch (Week 3)**:
+1. **Production** deployment
+2. **User** acceptance testing
+3. **Monitor** and optimize
+
+**Total Estimated Time**: 2-3 weeks for both teams working in parallel.
+
+---
+
+**Questions? Contact the backend team for node API details or clarification on the integration flow.**
\ No newline at end of file
diff --git a/src/features/incentive/PointSystem.ts b/src/features/incentive/PointSystem.ts
index 9cfc8769d..c5011f80c 100644
--- a/src/features/incentive/PointSystem.ts
+++ b/src/features/incentive/PointSystem.ts
@@ -11,7 +11,8 @@ import { Twitter } from "@/libs/identity/tools/twitter"
const pointValues = {
LINK_WEB3_WALLET: 0.5,
- LINK_TWITTER: 2,
+ LINK_TWITTER: 1,
+ LINK_TELEGRAM: 1,
LINK_GITHUB: 1,
FOLLOW_DEMOS: 1,
}
@@ -33,13 +34,17 @@ export class PointSystem {
*/
private async getUserIdentitiesFromGCR(userId: string): Promise<{
linkedWallets: string[]
- linkedSocials: { twitter?: string }
+ linkedSocials: { twitter?: string; telegram?: string }
}> {
const xmIdentities = await IdentityManager.getIdentities(userId)
const twitterIdentities = await IdentityManager.getWeb2Identities(
userId,
"twitter",
)
+ const telegramIdentities = await IdentityManager.getWeb2Identities(
+ userId,
+ "telegram",
+ )
const linkedWallets: string[] = []
@@ -63,12 +68,16 @@ export class PointSystem {
}
}
- const linkedSocials: { twitter?: string } = {}
+ const linkedSocials: { twitter?: string; telegram?: string } = {}
if (Array.isArray(twitterIdentities) && twitterIdentities.length > 0) {
linkedSocials.twitter = twitterIdentities[0].username
}
+ if (Array.isArray(telegramIdentities) && telegramIdentities.length > 0) {
+ linkedSocials.telegram = telegramIdentities[0].username
+ }
+
return { linkedWallets, linkedSocials }
}
@@ -194,7 +203,8 @@ export class PointSystem {
type === "socialAccounts" &&
(platform === "twitter" ||
platform === "github" ||
- platform === "discord")
+ platform === "discord" ||
+ platform === "telegram")
) {
const oldPlatformPoints =
account.points.breakdown?.socialAccounts?.[platform] || 0
@@ -610,6 +620,127 @@ export class PointSystem {
}
}
+ /**
+ * Award points for linking a Telegram account
+ * @param userId The user's Demos address
+ * @param referralCode Optional referral code
+ * @returns RPCResponse
+ */
+ async awardTelegramPoints(
+ userId: string,
+ referralCode?: string,
+ ): Promise {
+ try {
+ const userPointsWithIdentities = await this.getUserPointsInternal(
+ userId,
+ )
+
+ // Check if user already has Telegram points specifically
+ if ((userPointsWithIdentities.breakdown.socialAccounts as any).telegram > 0) {
+ return {
+ result: 200,
+ response: {
+ pointsAwarded: 0,
+ totalPoints: userPointsWithIdentities.totalPoints,
+ message: "Telegram points already awarded",
+ },
+ require_reply: false,
+ extra: {},
+ }
+ }
+
+ await this.addPointsToGCR(
+ userId,
+ pointValues.LINK_TELEGRAM,
+ "socialAccounts",
+ "telegram",
+ referralCode,
+ )
+
+ const updatedPoints = await this.getUserPointsInternal(userId)
+
+ return {
+ result: 200,
+ response: {
+ pointsAwarded: pointValues.LINK_TELEGRAM,
+ totalPoints: updatedPoints.totalPoints,
+ message: "Points awarded for linking Telegram",
+ },
+ require_reply: false,
+ extra: {},
+ }
+ } catch (error) {
+ return {
+ result: 500,
+ response: "Error awarding points",
+ require_reply: false,
+ extra: {
+ error:
+ error instanceof Error ? error.message : String(error),
+ },
+ }
+ }
+ }
+
+ /**
+ * Deduct points for unlinking a Telegram account
+ * @param userId The user's Demos address
+ * @returns RPCResponse
+ */
+ async deductTelegramPoints(userId: string): Promise {
+ try {
+ const userPointsWithIdentities = await this.getUserPointsInternal(
+ userId,
+ )
+
+ // Check if user has Telegram points to deduct
+ if (
+ ((userPointsWithIdentities.breakdown.socialAccounts as any).telegram || 0) <= 0
+ ) {
+ return {
+ result: 200,
+ response: {
+ pointsDeducted: 0,
+ totalPoints: userPointsWithIdentities.totalPoints,
+ message: "No Telegram points to deduct",
+ },
+ require_reply: false,
+ extra: {},
+ }
+ }
+
+ await this.addPointsToGCR(
+ userId,
+ -pointValues.LINK_TELEGRAM,
+ "socialAccounts",
+ "telegram",
+ )
+
+ const updatedPoints = await this.getUserPointsInternal(userId)
+
+ return {
+ result: 200,
+ response: {
+ pointsDeducted: pointValues.LINK_TELEGRAM,
+ totalPoints: updatedPoints.totalPoints,
+ message: "Points deducted for unlinking Telegram",
+ },
+ require_reply: false,
+ extra: {},
+ }
+ } catch (error) {
+ return {
+ result: 500,
+ response: "Error deducting points",
+ require_reply: false,
+ extra: {
+ error:
+ error instanceof Error ? error.message : String(error),
+ },
+ }
+ }
+ }
+
/**
* Deduct points for unlinking a GitHub account
* @param userId The user's Demos address
diff --git a/src/libs/abstraction/index.ts b/src/libs/abstraction/index.ts
index a8d06e23b..6e4e47838 100644
--- a/src/libs/abstraction/index.ts
+++ b/src/libs/abstraction/index.ts
@@ -1,9 +1,11 @@
import { GithubProofParser } from "./web2/github"
import { TwitterProofParser } from "./web2/twitter"
+import { TelegramProofParser } from "./web2/telegram"
import { type Web2ProofParser } from "./web2/parsers"
import { Web2CoreTargetIdentityPayload } from "@kynesyslabs/demosdk/abstraction"
import { hexToUint8Array, ucrypto } from "@kynesyslabs/demosdk/encryption"
import { Twitter } from "../identity/tools/twitter"
+import { Transaction } from "@kynesyslabs/demosdk/types"
/**
* Fetches the proof data using the appropriate parser and verifies the signature
@@ -11,11 +13,24 @@ import { Twitter } from "../identity/tools/twitter"
* @param payload - The proof payload
* @returns true if the proof is valid, false otherwise
*/
+/**
+ * Verifies Web2 identity proofs (Twitter, GitHub, Telegram)
+ *
+ * This function handles the verification of Web2 identity claims by:
+ * 1. Selecting the appropriate proof parser based on context
+ * 2. Performing context-specific validations (bot detection for Twitter)
+ * 3. Extracting and verifying the cryptographic signature
+ *
+ * @param payload - Web2 identity payload containing proof data
+ * @param sender - The Demos address claiming the identity
+ * @returns Verification result with success status and message
+ */
export async function verifyWeb2Proof(
payload: Web2CoreTargetIdentityPayload,
sender: string,
+ transaction?: Transaction,
) {
- let parser: typeof TwitterProofParser | typeof GithubProofParser
+ let parser: typeof TwitterProofParser | typeof GithubProofParser | typeof TelegramProofParser
switch (payload.context) {
case "twitter":
@@ -24,6 +39,11 @@ export async function verifyWeb2Proof(
case "github":
parser = GithubProofParser
break
+ case "telegram":
+ // REVIEW: Telegram proofs are handled differently - they come from bot attestations
+ // rather than user-posted content, but follow the same verification pattern
+ parser = TelegramProofParser
+ break
default:
return {
success: false,
@@ -57,6 +77,7 @@ export async function verifyWeb2Proof(
try {
const { message, type, signature } = await instance.readData(
payload.proof,
+ payload.context === "telegram" ? transaction : undefined,
)
try {
const verified = await ucrypto.verify({
@@ -89,4 +110,4 @@ export async function verifyWeb2Proof(
}
}
-export { TwitterProofParser, Web2ProofParser }
+export { TwitterProofParser, TelegramProofParser, Web2ProofParser }
diff --git a/src/libs/abstraction/web2/telegram.ts b/src/libs/abstraction/web2/telegram.ts
new file mode 100644
index 000000000..76a696330
--- /dev/null
+++ b/src/libs/abstraction/web2/telegram.ts
@@ -0,0 +1,115 @@
+import { Web2ProofParser } from "./parsers"
+import Telegram from "@/libs/identity/tools/telegram"
+import { SigningAlgorithm, Transaction } from "@kynesyslabs/demosdk/types"
+
+/**
+ * TelegramProofParser - Parses and validates Telegram identity proofs
+ *
+ * This parser handles the verification of Telegram identity claims by processing
+ * bot-attested verifications. Unlike Twitter proofs which are parsed from tweets,
+ * Telegram proofs are validated through a challenge-response mechanism with
+ * authorized bot attestation.
+ *
+ * Flow:
+ * 1. Bot receives signed challenge from user
+ * 2. Bot creates attestation with Telegram user data
+ * 3. This parser extracts and validates the proof from bot attestation
+ * 4. Returns signature data for identity verification
+ */
+export class TelegramProofParser extends Web2ProofParser {
+ private static instance: TelegramProofParser
+ telegram: Telegram
+
+ constructor() {
+ super()
+ this.telegram = Telegram.getInstance()
+ }
+
+ /**
+ * Reads and validates Telegram identity proof data
+ *
+ * For Telegram, the "proof" is the bot's attestation containing:
+ * - User's signed challenge
+ * - Telegram user data (ID, username)
+ * - Bot's signature of the attestation
+ *
+ * @param proofData - JSON string containing bot attestation data
+ * @param transaction - The transaction containing this proof (for challenge hash extraction)
+ * @returns Parsed signature data for verification
+ */
+ async readData(proofData: string, transaction?: Transaction): Promise<{
+ message: string
+ signature: string
+ type: SigningAlgorithm
+ }> {
+ try {
+ // REVIEW: For Telegram, the "proof" is actually the bot attestation data
+ // Parse the bot attestation containing the user's signed challenge
+ const attestationData = JSON.parse(proofData)
+
+ // Validate attestation structure
+ const requiredFields = ["telegram_id", "username", "signed_challenge", "timestamp", "bot_address", "bot_signature"]
+ for (const field of requiredFields) {
+ if (!attestationData[field]) {
+ throw new Error(`Missing required field: ${field}`)
+ }
+ }
+
+ // Extract challenge hash from transaction for replay protection validation
+ const transactionChallengeHash = transaction
+ ? Telegram.extractChallengeHashFromTransaction(transaction)
+ : null
+
+ // Verify the bot attestation first (this validates both signatures)
+ // IMPORTANT: Use 'validate' mode for on-chain validation
+ // This mode performs cryptographic verification and replay protection when transaction hash is available
+ const verificationResult = await this.telegram.verifyAttestation(
+ attestationData,
+ 'validate',
+ transactionChallengeHash
+ )
+
+ if (!verificationResult.success) {
+ throw new Error(`Telegram verification failed: ${verificationResult.message}`)
+ }
+
+ // Extract the user's signature from the signed challenge
+ // The signed_challenge format is: "challenge:signature" or just "signature"
+ const signedChallenge = attestationData.signed_challenge
+ const signaturePart = signedChallenge.includes(":")
+ ? signedChallenge.split(":")[1]
+ : signedChallenge
+
+ // Extract the original challenge message
+ const challengePart = signedChallenge.includes(":")
+ ? signedChallenge.split(":")[0]
+ : null
+
+ if (!challengePart) {
+ throw new Error("Invalid signed challenge format - missing challenge part")
+ }
+
+ // REVIEW: Return the signature data in the same format as TwitterProofParser
+ // This allows the identity system to verify the user's signature
+ return {
+ message: challengePart, // Original challenge that was signed
+ signature: signaturePart, // User's signature of the challenge
+ type: "ed25519" as SigningAlgorithm, // Demos uses ed25519 signatures
+ }
+
+ } catch (error) {
+ throw new Error(`Failed to parse Telegram proof: ${error instanceof Error ? error.message : String(error)}`)
+ }
+ }
+
+ /**
+ * Get singleton instance of TelegramProofParser
+ */
+ static async getInstance() {
+ if (!this.instance) {
+ this.instance = new TelegramProofParser()
+ }
+
+ return this.instance
+ }
+}
\ No newline at end of file
diff --git a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts
index 76538765d..f48b3a342 100644
--- a/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts
+++ b/src/libs/blockchain/gcr/gcr_routines/GCRIdentityRoutines.ts
@@ -234,6 +234,19 @@ export default class GCRIdentityRoutines {
editOperation.referralCode,
)
}
+ } else if (context === "telegram") {
+ const isFirst = await this.isFirstConnection(
+ "telegram",
+ { userId: data.userId },
+ gcrMainRepository,
+ editOperation.account,
+ )
+ if (isFirst) {
+ await IncentiveManager.telegramLinked(
+ editOperation.account,
+ editOperation.referralCode,
+ )
+ }
} else if (context === "github") {
const isFirst = await this.isFirstConnection(
"github",
@@ -295,10 +308,12 @@ export default class GCRIdentityRoutines {
await gcrMainRepository.save(accountGCR)
/**
- * Deduct incentive points for Twitter unlinking
+ * Deduct incentive points for unlinking social accounts
*/
if (context === "twitter") {
await IncentiveManager.twitterUnlinked(editOperation.account)
+ } else if (context === "telegram") {
+ await IncentiveManager.telegramUnlinked(editOperation.account)
} else if (context === "github" && removedIdentity && removedIdentity.userId) {
await IncentiveManager.githubUnlinked(
editOperation.account,
@@ -600,9 +615,9 @@ export default class GCRIdentityRoutines {
}
private static async isFirstConnection(
- type: "twitter" | "github" | "web3",
+ type: "twitter" | "github" | "web3" | "telegram",
data: {
- userId?: string // for twitter/github
+ userId?: string // for twitter and telegram/github
chain?: string // for web3
subchain?: string // for web3
address?: string // for web3
@@ -625,6 +640,25 @@ export default class GCRIdentityRoutines {
.andWhere("gcr.pubkey != :currentAccount", { currentAccount })
.getOne()
+ /**
+ * Return true if no account has this userId
+ */
+ return !result
+ } else if (type === "telegram") {
+ /**
+ * Check if this Telegram userId exists anywhere
+ */
+ const result = await gcrMainRepository
+ .createQueryBuilder("gcr")
+ .where(
+ "EXISTS (SELECT 1 FROM jsonb_array_elements(gcr.identities->'web2'->'telegram') as telegram_id WHERE telegram_id->>'userId' = :userId)",
+ {
+ userId: data.userId,
+ },
+ )
+ .andWhere("gcr.pubkey != :currentAccount", { currentAccount })
+ .getOne()
+
/**
* Return true if no account has this userId
*/
diff --git a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts
index 9209f3eb8..c248bfa23 100644
--- a/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts
+++ b/src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts
@@ -3,7 +3,7 @@ import { PointSystem } from "@/features/incentive/PointSystem"
/**
* This class is used to manage the incentives for the user.
- * It is used to award points to the user for linking their wallet, Twitter account, and GitHub account.
+ * It is used to award points to the user for linking their wallet, Twitter account, and Telegram account.
* It is also used to get the points for the user.
*/
export class IncentiveManager {
@@ -62,6 +62,26 @@ export class IncentiveManager {
return await this.pointSystem.deductTwitterPoints(userId)
}
+ /**
+ * Hook to be called after Telegram linking
+ */
+ static async telegramLinked(
+ userId: string,
+ referralCode?: string,
+ ): Promise {
+ return await this.pointSystem.awardTelegramPoints(
+ userId,
+ referralCode,
+ )
+ }
+
+ /**
+ * Hook to be called after Telegram unlinking
+ */
+ static async telegramUnlinked(userId: string): Promise {
+ return await this.pointSystem.deductTelegramPoints(userId)
+ }
+
/**
* Hook to be called after GitHub linking
*/
diff --git a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts
index c3bbe68ef..df04f979d 100644
--- a/src/libs/blockchain/gcr/gcr_routines/identityManager.ts
+++ b/src/libs/blockchain/gcr/gcr_routines/identityManager.ts
@@ -70,14 +70,16 @@ export default class IdentityManager {
sender: string,
payload: InferFromSignaturePayload,
): Promise<{ success: boolean; message: string }> {
- // INFO: Check if the user has a Twitter account
+ // INFO: Check if the user has a Twitter or Telegram account
const account = await ensureGCRForUser(sender)
const twitterAccounts = account.identities.web2["twitter"] || []
- if (twitterAccounts.length === 0) {
+ const telegramAccounts = account.identities.web2["telegram"] || []
+
+ if (twitterAccounts.length === 0 && telegramAccounts.length === 0) {
return {
success: false,
message:
- "Error: No Twitter account found. Please connect a Twitter account first",
+ "Error: No Twitter or Telegram account found. Please connect a social account first",
}
}
diff --git a/src/libs/identity/tools/TG_FLOW.md b/src/libs/identity/tools/TG_FLOW.md
new file mode 100644
index 000000000..f44bf9601
--- /dev/null
+++ b/src/libs/identity/tools/TG_FLOW.md
@@ -0,0 +1,202 @@
+# Telegram Identity Verification System
+
+## ASCII Flow Diagram
+
+```
+βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
+β User Client β β Telegram Bot β β Demos Backend β
+β β β (Authorized) β β (This Code) β
+βββββββββββ¬ββββββββ βββββββββββ¬βββββββββ βββββββββββ¬ββββββββ
+ β β β
+ β 1. Request Challenge β β
+ βββββββββββββββββββββββββββββββββββββββββββββββΊβ
+ β β β
+ β 2. Challenge Responseβ β
+ ββββββββββββββββββββββββββββββββββββββββββββββββ€
+ β β β
+ β 3. Sign Challenge β β
+ β (with Demos key) β β
+ β β β
+ β 4. Send to Bot β β
+ βββββββββββββββββββββββΊβ β
+ β β β
+ β β 5. Bot Verification β
+ β ββββββββββββββββββββββββΊβ
+ β β β
+ β β 6. Unsigned TX β
+ β βββββββββββββββββββββββββ€
+ β β β
+ β 7. Return TX β β
+ ββββββββββββββββββββββββ€ β
+ β β β
+ β 8. Sign & Submit TX β β
+ βββββββββββββββββββββββββββββββββββββββββββββββΊβ
+ β β β
+```
+
+## Data Structures Schema
+
+```
+TelegramChallenge {
+ challenge: string // "DEMOS_TG_BIND_{address}_{timestamp}_{nonce}"
+ demos_address: string // User's Demos blockchain address
+ timestamp: number // Unix timestamp when created
+ used: boolean // Whether challenge has been consumed
+}
+
+TelegramVerificationRequest {
+ bot_address: string // Ed25519 public key of authorized bot
+ telegram_id: string // Telegram user ID
+ username: string // Telegram username
+ signed_challenge: string // User-signed challenge (challenge:signature)
+ timestamp: number // Request timestamp
+ bot_signature: string // Bot's signature over attestation data
+}
+
+TelegramWeb2Payload {
+ context: string // "telegram"
+ proof: string // Bot attestation JSON string
+ username: string // Telegram username
+ userId: string // Telegram user ID
+ attestation_id: string // SHA256 hash of challenge for replay protection
+}
+
+TelegramVerificationResponse {
+ success: boolean
+ message: string
+ demosAddress?: string // User's Demos address (if successful)
+ telegramData?: {
+ userId: string
+ username: string
+ timestamp: number
+ }
+ unsignedTransaction?: Transaction // Ready for user to sign
+}
+```
+
+## Security Model
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β GENESIS BLOCK β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+β β Authorized Bot Addresses (Ed25519 Public Keys) β β
+β β - bot1_address: "abc123..." β β
+β β - bot2_address: "def456..." β β
+β β - bot3_address: "ghi789..." β β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β VERIFICATION CHAIN β
+β β
+β 1. Bot Authorization Check β β
+β βββ Genesis block lookup β
+β β
+β 2. Challenge Validation β β
+β βββ Format + Expiry + Usage check β
+β β
+β 3. Challenge Hash Anti-Replay Protection β β
+β βββ SHA256(challenge) embedded in transaction β
+β β
+β 4. Bot Signature Verification β β
+β βββ Ed25519 verify(attestation_data, bot_signature) β
+β β
+β 5. User Signature Verification β β
+β βββ Ed25519 verify(challenge, user_signature) β
+β β
+β 6. Identity Transaction Creation β
+β βββ Unsigned transaction for blockchain submission β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+## Challenge Format
+
+```
+Challenge Structure:
+"DEMOS_TG_BIND_{demos_address}_{timestamp}_{nonce}"
+
+Example:
+"DEMOS_TG_BIND_abc123def456_1692345678_9f8e7d6c5b4a3210"
+
+Components:
+βββββββββββ¬ββββββββββββββββββ¬ββββββββββββ¬ββββββββββββββββββ
+β Prefix β Demos Address β Timestamp β Random Nonce β
+βββββββββββΌββββββββββββββββββΌββββββββββββΌββββββββββββββββββ€
+β DEMOS_ β User's Ed25519 β Unix time β 16-byte hex β
+β TG_BIND β public key β creation β for uniqueness β
+βββββββββββ΄ββββββββββββββββββ΄ββββββββββββ΄ββββββββββββββββββ
+```
+
+## State Management
+
+```
+Memory Storage (Map):
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Challenge Key β Challenge Object β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β "DEMOS_TG_BIND_..." β { β
+β challenge: string, β
+β demos_address: string, β
+β timestamp: number, β
+β used: boolean β
+β } β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+Auto-cleanup: 15 minutes TTL per challenge
+Cache: Authorized bots (1 hour TTL)
+```
+
+---
+
+## System Overview
+
+This code implements a **Telegram identity verification system** for the Demos blockchain network. It allows users to cryptographically bind their Telegram accounts to their Demos blockchain addresses through a secure challenge-response protocol.
+
+### Key Components
+
+**1. Challenge Generation**
+- Creates unique challenges with format: `DEMOS_TG_BIND_{address}_{timestamp}_{nonce}`
+- 15-minute expiration window
+- One-time use only
+
+**2. Bot Authorization**
+- Only pre-authorized bots (stored in genesis block) can perform verifications
+- Bot addresses are Ed25519 public keys
+- Cached for performance (1-hour TTL)
+
+**3. Dual Signature Verification**
+- **User Signature**: User signs challenge with their Demos private key
+- **Bot Signature**: Bot signs attestation data proving Telegram identity
+
+**4. Identity Transaction Creation**
+- Generates unsigned blockchain transaction for identity binding
+- Follows Web2 identity pattern used by other platforms (Twitter, etc.)
+- User must sign and submit to complete the binding
+
+### Security Features
+
+- **Genesis-based Authorization**: Only pre-approved bots can verify identities
+- **Challenge Uniqueness**: Cryptographic nonces prevent replay attacks
+- **Challenge Hash Binding**: SHA256 challenge hash embedded in transactions
+- **Anti-Replay Protection**: Transaction validation requires matching challenge hash
+- **Dual Verification**: Both user and bot must provide valid signatures
+- **Time Limits**: Challenges expire after 15 minutes
+- **Single Use**: Each challenge can only be used once
+- **Validate Mode Security**: On-chain validation prevents replay of old attestations
+
+### Integration Flow
+
+1. User requests challenge from backend
+2. User signs challenge with their Demos private key
+3. User submits signed challenge to authorized Telegram bot
+4. Bot verifies user's Telegram identity and signs attestation
+5. Bot sends verification request to backend
+6. Backend validates both signatures and bot authorization
+7. Backend generates challenge hash (SHA256) for replay protection
+8. Backend returns unsigned identity transaction with embedded challenge hash
+9. User signs and submits transaction to blockchain
+10. On-chain validation verifies challenge hash matches transaction payload
+
+This creates a trustless bridge between Telegram identities and Demos blockchain addresses, with cryptographic protection against replay attacks.
diff --git a/src/libs/identity/tools/telegram.ts b/src/libs/identity/tools/telegram.ts
new file mode 100644
index 000000000..d798ac28d
--- /dev/null
+++ b/src/libs/identity/tools/telegram.ts
@@ -0,0 +1,476 @@
+import * as crypto from "crypto"
+import log from "@/utilities/logger"
+import Chain from "@/libs/blockchain/chain"
+import {
+ TelegramVerificationRequest,
+ TelegramVerificationResponse,
+ TelegramChallengeResponse,
+ Transaction,
+} from "@kynesyslabs/demosdk/types"
+// NOTE: The SDK currently has no exported InferFromTelegramPayload type (compile error showed only Twitter variant).
+// Define a minimal local interface mirroring expected web2 identity payload shape for Telegram.
+interface TelegramWeb2Payload {
+ context: 'telegram'
+ proof: string // bot attestation JSON string
+ username: string
+ userId: string
+ attestation_id?: string // Challenge hash for replay attack prevention
+}
+
+import { ucrypto, hexToUint8Array } from "@kynesyslabs/demosdk/encryption"
+
+/**
+ * Internal challenge storage interface
+ */
+interface TelegramChallenge {
+ challenge: string
+ demos_address: string
+ timestamp: number
+ used: boolean
+}
+
+/**
+ * Telegram identity verification tool
+ * Handles challenge generation, bot authorization, and signature verification
+ */
+export default class Telegram {
+ private static instance: Telegram
+ private challenges: Map = new Map()
+ private authorizedBots: string[] = []
+ private lastGenesisCheck = 0
+
+ private constructor() {
+ // Private constructor for singleton pattern
+ }
+
+ /**
+ * Get the singleton instance of the Telegram tool
+ */
+ static getInstance(): Telegram {
+ if (!Telegram.instance) {
+ Telegram.instance = new Telegram()
+ }
+ return Telegram.instance
+ }
+
+ /**
+ * Load authorized bot addresses from genesis block
+ * Results are cached for 1 hour since genesis never changes
+ */
+ async getAuthorizedBots(): Promise {
+ // Cache for 1 hour since genesis never changes
+ if (Date.now() - this.lastGenesisCheck < 3600000 && this.authorizedBots.length > 0) {
+ return this.authorizedBots
+ }
+
+ try {
+ const genesisBlock = await Chain.getGenesisBlock()
+ if (!genesisBlock || !genesisBlock.content) {
+ log.error("Genesis block not found or has no content")
+ return []
+ }
+ // genesisBlock.content is a JSON object (typeorm column json), NOT a string.
+ // The original genesis data we need (balances) is embedded as string in content.extra.genesisData.
+ let genesisData: any
+ if (typeof genesisBlock.content === "string") {
+ // (unlikely) stored as string JSON
+ genesisData = JSON.parse(genesisBlock.content)
+ } else if (
+ typeof genesisBlock.content === "object" &&
+ genesisBlock.content?.extra?.genesisData
+ ) {
+ // extra.genesisData is a JSON string created in generateGenesisBlock
+ try {
+ genesisData = JSON.parse(genesisBlock.content.extra.genesisData)
+ } catch (e) {
+ log.error("Failed to parse embedded genesisData string:" + e)
+ return []
+ }
+ } else {
+ // Fallback: maybe balances directly present
+ genesisData = genesisBlock.content
+ }
+
+ // Extract addresses from balances array
+ this.authorizedBots = genesisData.balances?.map((balance: [string, string]) =>
+ balance[0].toLowerCase(),
+ ) || []
+
+ this.lastGenesisCheck = Date.now()
+
+ log.info(`Loaded ${this.authorizedBots.length} authorized Telegram bot addresses from genesis`)
+ return this.authorizedBots
+ } catch (error) {
+ log.error("Failed to load authorized bots from genesis:"+ error)
+ return []
+ }
+ }
+
+ /**
+ * Check if a bot address is authorized (from genesis block)
+ */
+ async isAuthorizedBot(botAddress: string): Promise {
+ const authorizedBots = await this.getAuthorizedBots()
+ return authorizedBots.includes(botAddress.toLowerCase())
+ }
+
+ /**
+ * Generate a challenge for Telegram identity verification
+ * Format: DEMOS_TG_BIND___
+ */
+ generateChallenge(demosAddress: string): TelegramChallengeResponse {
+ const timestamp = Math.floor(Date.now() / 1000)
+ const nonce = crypto.randomBytes(16).toString("hex")
+ const challenge = `DEMOS_TG_BIND_${demosAddress}_${timestamp}_${nonce}`
+
+ // Store challenge for 15 minutes
+ this.challenges.set(challenge, {
+ challenge,
+ demos_address: demosAddress.toLowerCase(),
+ timestamp,
+ used: false,
+ })
+
+ // Auto-cleanup after 15 minutes
+ setTimeout(() => {
+ this.challenges.delete(challenge)
+ }, 15 * 60 * 1000)
+
+ log.info(`Generated Telegram challenge for address ${demosAddress}`)
+
+ return { challenge }
+ }
+
+ /**
+ * Parse a challenge to extract its components
+ */
+ private parseChallenge(challenge: string): {
+ demosAddress: string
+ timestamp: number
+ nonce: string
+ } | null {
+ const parts = challenge.split("_")
+ // Expected format: DEMOS_TG_BIND___
+ if (parts.length !== 6 || parts[0] !== "DEMOS" || parts[1] !== "TG" || parts[2] !== "BIND") {
+ return null
+ }
+ const demosAddress = parts[3]
+ const timestamp = parseInt(parts[4])
+ const nonce = parts[5]
+ if (isNaN(timestamp)) {
+ return null
+ }
+
+ return {
+ demosAddress,
+ timestamp,
+ nonce,
+ }
+ }
+
+ /**
+ * Verify a Telegram verification request from a bot
+ * This includes both bot signature verification and user signature verification
+ */
+ /**
+ * Verify a Telegram attestation.
+ * mode = 'attest' : interactive phase (bot hits /api/tg-verify). Challenge must exist & be unused; we then mark it used.
+ * mode = 'validate': on-chain validation phase (parser replays verification). We verify challenge reuse protection
+ * by checking the challenge hash embedded in the transaction payload.
+ */
+ async verifyAttestation(
+ request: TelegramVerificationRequest,
+ mode: 'attest' | 'validate' = 'attest',
+ transactionChallengeHash?: string, // For validate mode: hash from transaction payload
+ ): Promise {
+ try {
+ // 1. Check if bot address is authorized (from genesis)
+ if (!(await this.isAuthorizedBot(request.bot_address))) {
+ log.warning(`Unauthorized bot address attempted verification: ${request.bot_address}`)
+ return {
+ success: false,
+ message: "Unauthorized bot address",
+ }
+ }
+
+ // 2. Parse the challenge from user's signed message
+ const challengeInput = request.signed_challenge.split(":")[0] || request.signed_challenge
+
+ const challengeData = this.parseChallenge(challengeInput)
+ if (!challengeData) {
+ return {
+ success: false,
+ message: "Invalid challenge format",
+ }
+ }
+
+ // 3. Calculate challenge hash for replay protection
+ const originalChallenge = `DEMOS_TG_BIND_${challengeData.demosAddress}_${challengeData.timestamp}_${challengeData.nonce}`
+ const challengeHash = crypto.createHash('sha256').update(originalChallenge).digest('hex')
+
+ // 4. Validate challenge reuse protection based on mode
+ const storedChallenge = this.challenges.get(originalChallenge)
+
+ if (mode === 'attest') {
+ // Interactive mode: strict challenge validation
+ if (!storedChallenge) {
+ return {
+ success: false,
+ message: 'Challenge not found or expired',
+ }
+ }
+ if (storedChallenge.used) {
+ return {
+ success: false,
+ message: 'Challenge already used',
+ }
+ }
+ } else if (mode === 'validate') {
+ // If no transaction challenge hash is provided, reject the validation (no legacy txs expected)
+ if (!transactionChallengeHash) {
+ return {
+ success: false,
+ message: 'Transaction challenge hash missing - replay protection failed',
+ }
+ } else {
+ // Perform full replay protection validation
+ if (challengeHash !== transactionChallengeHash) {
+ return {
+ success: false,
+ message: 'Challenge hash mismatch - potential replay attack detected',
+ }
+ }
+ }
+ }
+
+ // 5. Verify bot signature
+ const attestationData = {
+ telegram_id: request.telegram_id,
+ username: request.username,
+ signed_challenge: request.signed_challenge,
+ timestamp: request.timestamp,
+ }
+ const attestationJson = JSON.stringify(attestationData, Object.keys(attestationData).sort())
+ const attestationMessage = new TextEncoder().encode(attestationJson)
+
+ const botSignatureValid = await ucrypto.verify({
+ algorithm: "ed25519",
+ signature: hexToUint8Array(request.bot_signature),
+ publicKey: hexToUint8Array(request.bot_address),
+ message: attestationMessage,
+ })
+
+ if (!botSignatureValid) {
+ log.warning(`Invalid bot signature from ${request.bot_address}`)
+ return {
+ success: false,
+ message: "Invalid bot signature",
+ }
+ }
+
+ // 6. Verify user signature against the original challenge
+ const challengeMessage = new TextEncoder().encode(originalChallenge)
+
+ // Extract signature from signed challenge (assuming format: "challenge:signature" or just signature)
+ const signaturePart = request.signed_challenge.includes(":")
+ ? request.signed_challenge.split(":")[1]
+ : request.signed_challenge
+
+ const userSignatureValid = await ucrypto.verify({
+ algorithm: "ed25519",
+ signature: hexToUint8Array(signaturePart),
+ publicKey: hexToUint8Array(challengeData.demosAddress),
+ message: challengeMessage,
+ })
+
+ if (!userSignatureValid) {
+ log.warning(`Invalid user signature for challenge from ${challengeData.demosAddress}`)
+ return {
+ success: false,
+ message: "Invalid user signature",
+ }
+ }
+
+ // 7. Mark challenge as used only during interactive attestation
+ if (storedChallenge && mode === 'attest') {
+ storedChallenge.used = true
+ }
+
+ // 8. Create unsigned identity transaction with challenge hash for replay protection
+ const unsignedTransaction = this.createIdentityTransaction(
+ challengeData.demosAddress,
+ request.telegram_id,
+ request.username,
+ JSON.stringify({
+ telegram_id: request.telegram_id,
+ username: request.username,
+ signed_challenge: request.signed_challenge,
+ timestamp: request.timestamp,
+ bot_address: request.bot_address,
+ bot_signature: request.bot_signature,
+ }),
+ challengeHash, // Add challenge hash to prevent replay attacks
+ )
+
+ // 8. Return success with unsigned transaction for user to sign
+ log.info(`Successfully verified Telegram identity: ${request.telegram_id} β ${challengeData.demosAddress}`)
+
+ return {
+ success: true,
+ message: "Telegram identity verified. Please sign the transaction to complete binding.",
+ demosAddress: challengeData.demosAddress,
+ telegramData: {
+ userId: request.telegram_id,
+ username: request.username,
+ timestamp: request.timestamp,
+ },
+ unsignedTransaction: unsignedTransaction,
+ }
+
+ } catch (error) {
+ log.error("Error verifying Telegram attestation:" + error)
+ return {
+ success: false,
+ message: "Internal verification error",
+ }
+ }
+ }
+
+ /**
+ * Creates an unsigned identity transaction for Telegram verification
+ *
+ * This follows the same pattern as Twitter identity transactions:
+ * - Transaction type: "identity"
+ * - Context: "web2"
+ * - Method: "web2_identity_assign"
+ * - Payload contains Telegram identity data and bot attestation proof
+ * - Challenge hash embedded for replay attack prevention
+ *
+ * @param demosAddress - User's Demos address
+ * @param telegramId - Telegram user ID
+ * @param username - Telegram username
+ * @param proofData - JSON string containing bot attestation
+ * @param challengeHash - SHA256 hash of the original challenge for replay protection
+ * @returns Unsigned transaction ready for user signature
+ */
+ private createIdentityTransaction(
+ demosAddress: string,
+ telegramId: string,
+ username: string,
+ proofData: string,
+ challengeHash: string,
+ ): Transaction {
+ const telegramPayload: TelegramWeb2Payload = {
+ context: "telegram",
+ proof: proofData, // Bot attestation containing all verification data
+ username: username,
+ userId: telegramId,
+ attestation_id: challengeHash, // Challenge hash for replay attack prevention
+ }
+
+ // Create transaction skeleton (same structure as Twitter identity transactions)
+ const transaction: Transaction = {
+ hash: "", // Will be calculated when signed
+ content: {
+ type: "identity",
+ from_ed25519_address: demosAddress,
+ from: demosAddress, // required by TransactionContent
+ to: demosAddress, // Identity transactions are self-directed
+ amount: 0, // No tokens transferred for identity binding
+ transaction_fee: { network_fee: 0, rpc_fee: 0, additional_fee: 0 },
+ data: [
+ "identity", // Transaction data type identifier
+ {
+ context: "web2", // Web2 identity context
+ method: "web2_identity_assign", // Identity assignment method
+ payload: telegramPayload, // Telegram-specific payload
+ },
+ ],
+ timestamp: Date.now(),
+ nonce: 0, // Will be set by transaction processing
+ gcr_edits: [], // Will be generated during transaction validation
+ },
+ signature: null, // User must sign this
+ ed25519_signature: null as any,
+ status: "pending" as any,
+ blockNumber: 0,
+ }
+
+ log.info(`Created unsigned Telegram identity transaction for ${demosAddress} β ${telegramId}`)
+ return transaction
+ }
+
+ /**
+ * Extract challenge hash from a transaction payload
+ * Used during validation mode to verify replay protection
+ */
+ static extractChallengeHashFromTransaction(transaction: Transaction): string | null {
+ try {
+ if (
+ transaction &&
+ transaction.content &&
+ Array.isArray(transaction.content.data) &&
+ transaction.content.data[0] === "identity" &&
+ (transaction.content.data[1] as { payload?: TelegramWeb2Payload })?.payload?.attestation_id
+ ) {
+ return (transaction.content.data[1] as { payload: TelegramWeb2Payload }).payload.attestation_id || null;
+ }
+ return null;
+ } catch {
+ return null;
+ }
+ }
+
+ /**
+ * Get statistics about current challenges (for debugging/monitoring)
+ */
+ getChallengeStats(): {
+ total: number
+ active: number
+ used: number
+ expired: number
+ } {
+ const now = Math.floor(Date.now() / 1000)
+ let active = 0
+ let used = 0
+ let expired = 0
+
+ for (const challenge of this.challenges.values()) {
+ if (challenge.used) {
+ used++
+ } else if (now - challenge.timestamp > 900) { // 15 minutes
+ expired++
+ } else {
+ active++
+ }
+ }
+
+ return {
+ total: this.challenges.size,
+ active,
+ used,
+ expired,
+ }
+ }
+
+ /**
+ * Clean up expired challenges (called periodically)
+ */
+ cleanupExpiredChallenges(): number {
+ const now = Math.floor(Date.now() / 1000)
+ let cleaned = 0
+
+ for (const [key, challenge] of this.challenges.entries()) {
+ if (now - challenge.timestamp > 900) { // 15 minutes
+ this.challenges.delete(key)
+ cleaned++
+ }
+ }
+
+ if (cleaned > 0) {
+ log.info(`Cleaned up ${cleaned} expired Telegram challenges`)
+ }
+
+ return cleaned
+ }
+}
\ No newline at end of file
diff --git a/src/libs/network/routines/transactions/handleIdentityRequest.ts b/src/libs/network/routines/transactions/handleIdentityRequest.ts
index c0c78a0fc..a523af2c2 100644
--- a/src/libs/network/routines/transactions/handleIdentityRequest.ts
+++ b/src/libs/network/routines/transactions/handleIdentityRequest.ts
@@ -81,6 +81,7 @@ export default async function handleIdentityRequest(
return await verifyWeb2Proof(
payload.payload as Web2CoreTargetIdentityPayload,
sender,
+ tx, // Pass the transaction for Telegram challenge hash validation
)
case "xm_identity_remove":
case "pqc_identity_remove":
diff --git a/src/libs/network/server_rpc.ts b/src/libs/network/server_rpc.ts
index 6d87d5d9f..f3566ba9d 100644
--- a/src/libs/network/server_rpc.ts
+++ b/src/libs/network/server_rpc.ts
@@ -31,6 +31,8 @@ import { manageNativeBridge } from "./manageNativeBridge"
import Chain from "../blockchain/chain"
import { RateLimiter } from "./middleware/rateLimiter"
import GCR from "../blockchain/gcr/gcr"
+import Telegram from "../identity/tools/telegram"
+import { TelegramChallengeRequest, TelegramVerificationRequest } from "@kynesyslabs/demosdk/types"
// Reading the port from sharedState
const noAuthMethods = ["nodeCall"]
@@ -389,6 +391,78 @@ export async function serverRpcBun() {
return jsonResponse(rateLimiter.getStats())
})
+ // REVIEW: Telegram identity verification endpoints
+ // Generate challenge for Telegram verification
+ server.post("/api/tg-challenge", async req => {
+ try {
+ const payload = await req.json()
+
+ // Validate request structure
+ if (!payload.demos_address || typeof payload.demos_address !== "string") {
+ return jsonResponse({
+ error: "Invalid request: demos_address is required",
+ }, 400)
+ }
+
+ const telegramTool = Telegram.getInstance()
+ const challengeResponse = telegramTool.generateChallenge(payload.demos_address)
+
+ return jsonResponse(challengeResponse)
+ } catch (error) {
+ log.error("[Telegram] Error generating challenge: " + error)
+ return jsonResponse({
+ error: "Internal error generating challenge",
+ }, 500)
+ }
+ })
+
+ // Verify Telegram attestation from bot and create unsigned identity transaction
+ server.post("/api/tg-verify", async req => {
+ try {
+ const payload = await req.json()
+
+ // Validate request structure - check all required fields
+ const requiredFields = ["telegram_id", "username", "signed_challenge", "timestamp", "bot_address", "bot_signature"]
+ for (const field of requiredFields) {
+ if (!payload[field]) {
+ return jsonResponse({
+ error: `Invalid request: ${field} is required`,
+ }, 400)
+ }
+ }
+
+ const telegramTool = Telegram.getInstance()
+ const verificationResponse = await telegramTool.verifyAttestation(payload as TelegramVerificationRequest)
+
+ // REVIEW: New flow - return unsigned transaction for user to sign
+ // This follows the transaction-based pattern like Twitter identities
+ if (verificationResponse.success) {
+ log.info(`[Telegram] Verification successful, returning unsigned transaction for ${verificationResponse.demosAddress}`)
+
+ return jsonResponse({
+ success: true,
+ message: verificationResponse.message,
+ demosAddress: verificationResponse.demosAddress,
+ telegramData: verificationResponse.telegramData,
+ unsignedTransaction: verificationResponse.unsignedTransaction,
+ }, 200)
+ } else {
+ log.warning(`[Telegram] Verification failed: ${verificationResponse.message}`)
+
+ return jsonResponse({
+ success: false,
+ message: verificationResponse.message,
+ }, 400)
+ }
+
+ } catch (error) {
+ log.error("[Telegram] Error verifying attestation: " + error)
+ return jsonResponse({
+ error: "Internal error verifying attestation",
+ }, 500)
+ }
+ })
+
// Main RPC endpoint
server.post("/", async req => {
try {
diff --git a/src/utilities/validateUint8Array.ts b/src/utilities/validateUint8Array.ts
index f7b545730..4303b1e89 100644
--- a/src/utilities/validateUint8Array.ts
+++ b/src/utilities/validateUint8Array.ts
@@ -1,9 +1,9 @@
export default function validateIfUint8Array(input: unknown): Uint8Array | unknown {
- if (typeof input === 'object' && input !== null) {
+ if (typeof input === "object" && input !== null) {
const txArray = Object.keys(input)
.sort((a, b) => Number(a) - Number(b))
.map(k => input[k])
return Buffer.from(txArray)
}
- return input;
+ return input
}