Proposition pour obtenir facilement le solde d'un compte sans indexeur

Merge request GitLab associée à la proposition: Resolve "Create a runtime API UniversalDividendApi_account_balances to retrieve total and transferable account balances" (!328) · Merge requests · nodes / rust / Duniter v2S · GitLab

Contexte et problème

Aujourd’hui, la plupart des wallets et applications tierces s’appuient sur un indexeur (par exemple un service GraphQL) pour récupérer la balance d’un compte.

  • Pour un utilisateur, savoir « combien de Ğ1 j’ai » est critique : cela doit fonctionner même si l’indexeur est hors-service ou en retard.
  • En l’absence d’indexeur, certaines applications peuvent ne pas permettre d’initier un transfert, car elles imposent de connaître d’abord le solde transférable. Or l’utilisateur doit pouvoir initier un transfert sans indexeur.

Proposition

Ajouter une runtime API nommée UniversalDividendApi_account_balances qui renvoie, pour un AccountId, les champs suivants :

{
  /// Total des fonds dont l’utilisateur est propriétaire
  total: number,

  /// Fonds réellement transférables (inclut les DUs non réclamés, mais exclut le « dépôt d’existence » et les fonds réservés)
  transferable: number,

  /// Montant total des DUs non-réclamés
  unclaim_uds: number
}

Pour rappel, toutes les runtime APIs sont des fonctions « read-only » appelables via RPC avec la méthode state_call(apiName, params). La convention substrate pour le nom des runtime API est NameApi_function_name.

Comme leur nom l’indique, les runtime APIs peuvent être ajoutées ou modifiées uniquement par un changement de runtime ; il n’est pas nécessaire de livrer un nouveau binaire de duniter-v2s.

Exemples d’appel avec curl, Polkadot.js et PAPI

:warning: Ces exemples ne fonctionne pas sur la GDev actuelle car le runtime 900 n’inclus pas la MR !328. Pour tester il vous faut une chaine locale avec un runtime qui inclus cette MR.

Curl

RPC_URL="http://127.0.0.1:9933"
HEX_ACCOUNT="0x0123…ef" # Clé publique paddée à 32 octets et encodée en hexadécimal

curl -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "id":1,
    "method":"state_call",
    "params":[
      "UniversalDividendApi_account_balances",
      "'"${HEX_ACCOUNT}"'"
    ]
  }'

Polkadot.js

import { ApiPromise, WsProvider } from '@polkadot/api';

async function getBalances(ss58) {
  const api = await ApiPromise.create({ provider: new WsProvider('ws://127.0.0.1:9944') });
  const hex = api.createType('AccountId', ss58).toHex();
  const raw = await api.rpc.state.call('UniversalDividendApi_account_balances', hex);
  const [ total, transferable, unclaimUds ] = api
    .createType('(u64,u64,u64)', raw)
    .toJSON();
  console.log({ total, transferable, unclaimUds });
}

getBalances('5F3sa2TJAWMqDhXG6jhV4N8ko9E…');

PAPI

import { createClient } from 'polkadot-api';
import { getWsProvider } from 'polkadot-api/ws-provider/web';

async function getBalances(ss58: string) {
  const client = createClient(getWsProvider('ws://127.0.0.1:9944'));
  const api = client.getTypedApi(gdev);
  const { total, transferable, unclaimAmount } =
    await api.apis.UniversalDividendApi.accountBalances(ss58);
  console.log(total.toString(), transferable.toString(), unclaimAmount.toString());
}

getBalances('5F3sa2TJAWMqDhXG6jhV4N8ko9E…');

Si cela vous donne des idées d’autres runtimes API qui serait utiles pour les wallet et applications, merci de créer un nouveau sujet par runtime API proposée.

7 Likes

Les wallets devraient quand même l’autoriser, en utilisant transfer_keep_alive.

2 Likes

Le problème, c’est que si un wallet soumet un transfer_keep_alive en essayant de transférer plus que le solde transférable, la transaction sera quand même incluse en blockchain et consommera malgré tout les quotas de l’utilisateur (voire ponctionnera des frais de transaction si aucun quota gratuit n’est disponible).

En effet, du point de vue de Substrate, la transaction en elle-même est parfaitement valide, même si l’exécution du runtime call transfer_keep_alive retourne une erreur.

C’est pourquoi les wallets doivent connaître leur solde transférable avant d’envoyer un transfer.

4 Likes

Cette nouvelle runtime API est disponible sur la ĞTest.

Je viens de pousser des commits dans gcli (branche gtest) pour l’utiliser, et ça fonctionne :

Screenshot 2025-07-11 at 13.32.51

cc @poka, @ManUtopiK @vit , @vjrj

Le premier post de ce sujet explique comment appeler cette nouvelle runtime API avec curl, ainsi qu’avec les librairies PolkadotJS et PAPI.

Je vous recommande d’afficher à l’utilisateur le champ total retourné par la runtime API.

Ce champ inclut les DU non réclamés ainsi que les fonds éventuellement bloqués. Relisez le premier post pour bien comprendre la signification de chaque champ.

5 Likes

Découverte runtime API

Je ne trouve pas cette API runtime avec py-polkadot-sdk :

from substrateinterface import SubstrateInterface
from scalecodec.base import ScaleBytes

rpc = SubstrateInterface("wss://gt.elo.tf")

rpc.runtime_call("UniversalDividendApi", "account_balances")
*** ValueError: Runtime API Call 'UniversalDividendApi.account_balances' not found in registry

La méthode runtime_call() (doc) interroge pour savoir si l’API et la méthode existent. Ça lui permet de formater la requête et le retour avec les types définis dans la définition. Cependant, cette dernière n’est pas définie dans l’API rpc de Duniter v2s.

Voici les API runtime actuellement définies :

list(rpc.runtime_config.type_registry["runtime_api"])
['AccountNonceApi', 'AuthorityDiscoveryApi', 'BlockBuilder', 'ContractsApi', 'ConvertTransactionRuntimeApi', 'Core', 'EthereumRuntimeRPCApi', 'Metadata', 'NominationPoolsApi', 'SessionKeys', 'TransactionPaymentApi', 'TransactionPaymentCallApi']

Si j’utilise une API définie avec runtime_call() ça fonctionne :

rpc.runtime_config.type_registry["runtime_api"]["AccountNonceApi"]
{'methods': {'account_nonce': {'description': 'The API to query account nonce (aka transaction index)', 'params': [{'name': 'account_id', 'type': 'AccountId'}], 'type': 'Index'}}}

rpc.runtime_call("AccountNonceApi", "account_nonce", {"account_id": address})
<U32(value=7)>

En bricolant un peu, j’arrive à utiliser l’API UniversalDividendApi_account_balances (clé publique en hexadécimale obtenue avec https://ss58.org/ et type de retour trouvé dans l’implémentation et ci-dessus dans Polkadot.js) :

result_data = rpc.rpc_request("state_call", ["UniversalDividendApi_account_balances", "0xe8affd28ba034e4dbc6d08e82da22e62fc336c89ef45dc5d4b1cfa5b3e3fed20"])
result_data
{'jsonrpc': '2.0', 'id': 2, 'result': '0x88718300000000002471830000000000204e000000000000'}
result_obj = rpc.runtime_config.create_scale_object("(u64,u64,u64)")
result_obj.decode(ScaleBytes(result_data['result']), check_remaining=rpc.config.get('strict_scale_decode'))
(8614280, 8614180, 20000)

Mêmes résultats qu’avec Ğcli :

gcli-v2s (gtest)> ./target/release/gcli -v moul account balance
5HKoFDvEQejcx4GiH6PRx93sBNPqEN6q991SZpQgHCXVUHtN has 86142.80 ĞT total, 86141.80 ĞT transferable, 200.00 ĞT unclaimed UD

Documenter cette fonctionnalité

Ça serait bien de documenter cette API sur le site web pour que les futurs développeurs qui débarqueront se retrouvent. Je considère qu’une implémentation pour utilisateurs doit être accompagnée de documentation. Sur un post de ce forum, ça se perd ensuite dans la foule d’informations.

J’ai tenté d’ajouter la définition de UniversalDividendApi dans RuntimeApiCollection.

Ça n’est pas testé. J’attends vos retours.