# MonToken Scores — contrat mobile v1 État : API v1 déployée et vérifiée en HTTPS sur Whisky. La disponibilité se vérifie avec GET /api/v1/config. ## Adresse et réseau Base API : https://scores.ddx.im/api/v1 Classement web : https://scores.ddx.im/ Réseau : Whisky — MonToken ; chain ID 20261024 (0x13528a0). MTK natif, 18 décimales. Genesis : 0xb37fe8725aa38647cafb7f65ca148aa3930f0a56ec73fd334595a4cf58b206eb. RPC pour les transferts : https://rpc.ddx.im/ (distinct de l’API scores). Le serveur scores ne réalise aucun transfert, ne distribue aucun MTK et ne détient aucune clé de portefeuille. Il conserve seulement les pseudos/adresses et les demandes temporaires. Il ne lit pas les secrets de la réserve. ## Fonction à ajouter à l’app iOS / Android Bouton « M’inscrire au tableau des scores » : 1. Utiliser le wallet actuellement choisi dans l’app. Ne pas en créer un nouveau. 2. Demander le pseudo et expliquer que pseudo, adresse et score deviennent publics. 3. POST registration/challenge avec username et address. 4. Vérifier les informations reçues et présenter une confirmation explicite du domaine scores.ddx.im, du pseudo, du wallet et de l’absence de transfert. 5. Signer exactement le champ message avec la clé locale : Ethereum personal_sign / EIP-191 (secp256k1). 6. POST registration/complete avec challengeId et signature. 7. En cas de HTTP 201, enregistrer localement que le wallet est inscrit et afficher le classement. Aucun mot de passe, email, cookie ou jeton API. L’inscription ne remplace pas l’identité du wallet et ne donne pas de pouvoir de dépense au serveur. Seule une adresse EVM classique (EOA) est gérée dans cette v1 ; pas de wallet contractuel EIP-1271. ## Signature : impératif commun aux deux développeurs Le serveur produit un message au format SIWE / EIP-4361 pour une inscription de wallet EOA. Il ne crée pas de session web de connexion. Signer le texte UTF-8 EXACT reçu dans message, avec les sauts de ligne LF reçus par JSON. Ne pas le reconstruire, le traduire ou le normaliser. Aucun saut de ligne final supplémentaire. Digest attendu : keccak256(0x19 || "Ethereum Signed Message:\n" || longueur décimale EN OCTETS UTF-8 || octets UTF-8 du message) Le préfixe EIP-191 doit être appliqué une seule fois. Utiliser la fonction de signature de message Ethereum de la bibliothèque ; ne pas lui passer un digest comme s’il s’agissait du texte original. Ce n’est ni une transaction, ni EIP-712, ni une signature de hash de transaction. Signature retournée : 65 octets r || s || v en hexadécimal avec préfixe 0x (130 caractères hexadécimaux après 0x), v = 27 ou 28 recommandé (0/1 également reconnu par ethers). Le serveur récupère le signataire et le compare au wallet demandé. Exemple de structure, valeurs temporaires à ne jamais coder en dur : ```text scores.ddx.im wants you to sign in with your Ethereum account: Register wallet as Alice on the MonToken leaderboard. No transaction or transfer. URI: https://scores.ddx.im/ Version: 1 Chain ID: 20261024 Nonce: <32 caractères hexadécimaux aléatoires> Issued At: Expiration Time: Request ID: ``` Dans l’app : accepter uniquement la base HTTPS attendue, le domaine scores.ddx.im, chainId 20261024, signatureType personal_sign, le wallet courant et le pseudo demandé. Vérifier également la cohérence du message (domaine, URI, adresse, pseudo et Chain ID). Refuser un challenge expiré. Ne pas se fier à un domaine quelconque transmis par un lien externe. Références : https://eips.ethereum.org/EIPS/eip-4361 et https://docs.ethers.org/v6/api/hashing/ (hashMessage / verifyMessage). ## Routes ### GET /config HTTP 200 : apiVersion, domain, network (name, chainId, symbol, decimals, rpcUrl, genesisHash), registration (signatureType, expiresInSeconds, usernamePattern, usernameCaseSensitive, maxParticipants), scoreType, leaderboardCacheSeconds. ### POST /registration/challenge Content-Type: application/json ```json {"username":"Alice","address":""} ``` HTTP 201 : ```json { "challengeId":"", "address":"", "username":"Alice", "message":"", "expiresAt":"", "chainId":20261024, "domain":"scores.ddx.im", "signatureType":"personal_sign" } ``` Pseudo : 3 à 20 caractères ASCII, expression ^[A-Za-z0-9][A-Za-z0-9_-]{2,19}$. Unicité insensible à la casse ; affichage conservé (Alice et alice ne peuvent pas être deux joueurs différents). Aucune réservation du pseudo avant validation de la signature. Un wallet, un pseudo ; la réserve est exclue. Ne pas envoyer de champs de secret ou de solde. ### POST /registration/complete ```json {"challengeId":"","signature":"0x"} ``` HTTP 201 : ```json {"player":{"address":"","username":"Alice","registeredAt":""}} ``` Le challenge expire après 300 secondes. Une validation consomme définitivement le challenge. Cinq signatures erronées verrouillent ce challenge. Une réponse réseau perdue ne justifie pas une nouvelle inscription aveugle : consulter GET /players/{address}. Aucun changement de pseudo, suppression, connexion persistante ni récupération de compte n’est inclus dans cette v1. ### GET /players/{address} HTTP 200 : {"player":{"address":"...","username":"...","registeredAt":"..."}}. HTTP 404 PLAYER_NOT_FOUND : wallet non inscrit. Cette route est publique et permet à l’app de retrouver l’inscription après une réinstallation ou une restauration du wallet. Il n’est pas nécessaire de signer à nouveau pour la consultation. ### GET /leaderboard HTTP 200 : ```json { "network":{"name":"Whisky — MonToken","chainId":20261024,"symbol":"MTK","decimals":18,"rpcUrl":"https://rpc.ddx.im/","genesisHash":"0xb37fe8725aa38647cafb7f65ca148aa3930f0a56ec73fd334595a4cf58b206eb"}, "scoreType":"native_balance", "blockNumber":123, "blockHash":"0x", "blockTimestamp":"", "updatedAt":"", "players":[{"rank":1,"address":"","username":"Alice","balanceWei":"1000000000000000000","balanceMTK":"1.0","registeredAt":""}] } ``` Cet exemple illustre le schéma et ne représente pas des données réelles. Les soldes sont tous lus au même bloc ; updatedAt est l’heure de lecture, distincte de blockTimestamp. Le classement est décroissant, sans perte de précision. En cas d’égalité, même rang (1,1,3) et ordre alphabétique déterministe. balanceWei et balanceMTK sont des CHAÎNES ; ne pas utiliser Float/Double pour les montants. Seuls les wallets inscrits apparaissent, sans la réserve. Le cache serveur est partagé pendant 15 secondes, invalidé après inscription. Il n’y a pas de filtre ni pagination serveur dans ce POC (maximum 1000 participants). L’app peut rechercher dans la liste reçue. Actualiser toutes les 15 à 30 secondes au premier plan et arrêter le polling en arrière-plan. En cas de 503, conserver éventuellement les dernières valeurs en indiquant clairement qu’elles sont anciennes ; ne pas les remplacer par zéro. Le score est actuellement le solde natif MTK : les transferts modifient donc le classement. Il n’existe encore ni points distincts attribués par l’organisateur, ni historique de scores, ni indexeur, ni notification push. ### GET /health HTTP 200 : {"status":"ok","service":"mtk-scores","apiVersion":1}. Cette route vérifie le processus API, pas la blockchain ; /leaderboard vérifie le RPC, le Chain ID et le bloc initial. ## Erreurs Réponses applicatives : {"error":{"code":"CODE","message":"Explication en français"}}. Le proxy peut produire une réponse non JSON (p. ex. 413/502) : la gérer proprement. - 400 INVALID_ADDRESS / INVALID_USERNAME / INVALID_CHALLENGE : saisie invalide. - 400 RESERVE_EXCLUDED : le portefeuille de réserve est hors classement. - 401 INVALID_SIGNATURE : signature malformée, mauvais wallet ou message différent. - 403 ORIGIN_NOT_ALLOWED : origine web différente ; les appels natifs sans Origin sont autorisés. - 404 CHALLENGE_NOT_FOUND / PLAYER_NOT_FOUND / NOT_FOUND. - 409 USERNAME_TAKEN : choisir un autre pseudo. - 409 ALREADY_REGISTERED : consulter /players/{address}. - 409 CHALLENGE_USED : consulter /players/{address}, sinon recréer un challenge. - 410 CHALLENGE_EXPIRED : demander un nouveau challenge. - 413 BODY_TOO_LARGE : corps maximal de 8 Ko. - 415 CONTENT_TYPE : Content-Type doit être application/json. - 429 RATE_LIMITED : Retry-After: 60. Limites par IP : 30 requêtes POST/minute et 120 GET/minute. - 429 CHALLENGE_LOCKED : demander un nouveau challenge après trop de mauvaises signatures. - 503 RPC_UNAVAILABLE / NETWORK_MISMATCH / NETWORK_CHANGED : afficher indisponibilité sans ignorer les vérifications réseau. - 503 REGISTRATION_LIMIT / CHALLENGE_LIMIT : capacité temporairement atteinte. - 500 INTERNAL_ERROR : ne pas afficher de secret ; proposer de réessayer. ## Tests de recette iOS / Android 1. Créer un wallet local, vérifier qu’il est sur MonToken et qu’il n’est pas déjà inscrit. 2. Inscrire un pseudo neuf ; vérifier signature personnelle et HTTP 201. 3. Retrouver pseudo/adresse dans /players et dans le classement web et mobile. 4. Vérifier pseudo déjà pris (y compris casse différente), mauvais signataire, message altéré, expiration et réutilisation. 5. Approvisionner le wallet par un transfert distinct explicitement demandé ; vérifier l’évolution du score. 6. Vérifier erreurs réseau et absence de fuite des clés/signatures dans les logs et télémétries. 7. Restaurer le même wallet et retrouver l’inscription sans création de doublon. Ne jamais transmettre clé privée ou phrase de récupération au serveur ou aux autres développeurs.