API des soldes de tokens de wallet : actifs ERC-20 et historique des transferts

Créez une page des actifs de wallet avec les soldes de tokens ERC-20 non nuls, l'historique des transferts et les métadonnées par lots. Vérifiez la couverture de la chaîne, paginez les résultats et ajustez les montants entiers selon les décimales.

Créez une page des actifs de wallet avec l'API de données de wallet blockchain : utilisez l'API des soldes de tokens pour les avoirs ERC-20 non nuls et l'API des transferts de tokens pour l'historique du wallet. Les développeurs et les agents IA utilisent les mêmes requêtes authentifiées. Avant d'interroger, lisez GET /v1/status et vérifiez les champs data_features et data_status de la chaîne sélectionnée ; la couverture des soldes varie selon la chaîne. Les paramètres de requête et les schémas de réponse se trouvent dans la référence de la Data API.

Tâches que ce guide vous aide à accomplir

Les trois types de données nécessaires à une page des actifs de wallet

Une page des actifs de wallet peut afficher les soldes de tokens ERC-20 d'une adresse, l'historique des transferts de tokens et les métadonnées des tokens. La Data API fournit un point de terminaison pour chacun d'eux :

  • Soldes : GET /{chain}/addresses/{address}/balances renvoie les soldes ERC-20 non nuls de l'adresse, triés par adresse de token croissante, avec le symbol et les decimals du token inclus lorsqu'ils sont disponibles. Une adresse sans solde renvoie 200 avec data: [].
  • Transferts : GET /{chain}/addresses/{address}/transfers renvoie les transferts de tokens impliquant l'adresse dans une fenêtre de blocs requise, triés par (block_number, log_index) décroissants.
  • Métadonnées de tokens : GET /{chain}/tokens/{token} lit le nom, le symbole, les décimales et l'offre totale d'un token par son adresse de contrat ; POST /{chain}/tokens:batch lit les mêmes métadonnées pour un maximum de 100 adresses en une seule requête.

Tous trois utilisent https://api.blockvectra.com/v1/data comme URL de base et l'en-tête de requête x-api-key, avec robinhood_mainnet comme chaîne d'exemple. Ils appartiennent respectivement aux fonctionnalités balances, transfers et token_metadata ; pour connaître les chaînes proposant chaque fonctionnalité, consultez la page des chaînes prises en charge. Sur une chaîne ne disposant pas de la fonctionnalité, le point de terminaison renvoie 422 no_coverage.

Requête 1 : soldes d'adresses

Ce point de terminaison prend moins de paramètres, ce qui en fait une bonne première requête pour une page :

  • {chain} (paramètre de chemin, obligatoire) : identifiant de chaîne, la valeur chain d'une entrée dans GET /chains (par exemple robinhood_mainnet). La correspondance est exacte et sensible à la casse ; les alias et les identifiants numériques de chaîne ne sont pas acceptés.
  • {address} (paramètre de chemin, obligatoire) : adresse de 20 octets ; le préfixe 0x est facultatif et la casse majuscule ou minuscule est acceptée.
  • limit (paramètre de requête, facultatif) : taille de la page. Vaut 50 par défaut ; les valeurs supérieures à 500 sont ramenées à 500 ; 0 ou un non-entier renvoie 400 bad_request.
  • cursor (paramètre de requête, facultatif) : le next_cursor de la réponse précédente, renvoyé tel quel pour récupérer la page suivante. Un curseur n'est valide que pour la chaîne, le point de terminaison et les paramètres de requête qui l'ont émis ; le réutiliser ailleurs renvoie 400 bad_request.

Il utilise une pagination par jeu de clés (keyset pagination) : next_cursor n'apparaît que lorsqu'il y a une page suivante. Sur la dernière page, la clé est entièrement absente, jamais null.

export BLOCKVECTRA_API_KEY=rgw_your_api_key

curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/balances?limit=50" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

L'enveloppe de réponse est AddressBalanceListEnvelope, contenant data et meta. Chaque élément de data est un AddressBalance :

ChampTypeDescription
tokenstring (adresse)Adresse du contrat de token ; la forme canonique est 0x suivi de 40 chiffres hexadécimaux en minuscules.
balancestring (décimal)Solde entier brut, pouvant dépasser 2^53, renvoyé sous la forme d'une chaîne décimale brute — jamais un nombre JSON, une notation scientifique ou de l'hexadécimal.
symbolstring ou nullSymbole du token, ou null lorsqu'il est indisponible.
decimalsinteger ou nullDécimales du token, de 0 à 255, ou null lorsqu'elles sont indisponibles.

Requête 2 : transferts d'adresses

Le point de terminaison des transferts nécessite une fenêtre de blocs explicite : from_block et to_block sont tous deux obligatoires et doivent satisfaire from_block <= to_block. Il prend quelques paramètres supplémentaires :

  • standard (paramètre de requête, obligatoire) : erc20 ou erc721. Les requêtes restreintes à une adresse ne couvrent pas erc1155 ; le transmettre renvoie 422 no_coverage.
  • direction (paramètre de requête, facultatif) : in, out ou any ; vaut any par défaut et filtre par direction relative à l'adresse.
  • token (paramètre de requête, facultatif) : restreint les résultats à un contrat de token.
  • clamp (paramètre de requête, facultatif) : seule la chaîne littérale true l'active ; toute autre valeur est traitée comme false.

Bornes de la fenêtre et finalité : un to_block explicite supérieur à as_of_block renvoie 409 not_indexed_yet à moins que clamp=true ne le tronque à as_of_block ; une fenêtre plus large que la limite de la chaîne (limits.max_window_blocks issu de GET /chains) renvoie 409 window_too_large à moins que clamp=true ne la tronque du côté le plus ancien (en augmentant from_block et en maintenant to_block fixe). Si from_block dépasse déjà lui-même as_of_block, l'erreur 409 persiste même avec clamp=true. Lorsque la fenêtre est tronquée ou partiellement couverte, le champ meta.coverage de la réponse vaut "partial" ; sinon, il vaut "full".

Dans les enregistrements de transferts, les éléments ERC-20 ajoutent amount ; les éléments ERC-721 ajoutent token_id. Les deux incluent token, standard, from, to, block_number, block_timestamp, tx_hash, tx_index et log_index.

export BLOCKVECTRA_API_KEY=rgw_your_api_key

# 1) Lire as_of_block depuis la réponse des soldes.
AS_OF_BLOCK=$(curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/balances" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" | grep -o '"as_of_block":[0-9]*' | cut -d: -f2)

# 2) clamp=true tronque une fenêtre trop large ou un to_block supérieur à as_of_block, au lieu de renvoyer 409.
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/transfers?standard=erc20&from_block=0&to_block=$AS_OF_BLOCK&direction=any&clamp=true" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

Parcourir tous les transferts avec la pagination

Le champ next_cursor du point de terminaison des transferts d'adresses est optimiste : il n'apparaît que lorsque la page a renvoyé exactement limit lignes, de sorte qu'une page peut comporter un next_cursor tout en s'avérant être la dernière page. Ne vous arrêtez pas lorsqu'une page est vide ; suivez next_cursor jusqu'à ce que la clé soit absente.

Le code ci-dessous récupère chaque transfert dans la fenêtre :

const address = "0x1111111111111111111111111111111111111111";
const head = await fetch(
  `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/balances`,
  { headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! } },
).then((r) => r.json());
const asOfBlock = head.meta.as_of_block;
const transfers: unknown[] = [];
let cursor: string | undefined;

do {
  const url = new URL(
    `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/transfers`,
  );
  url.searchParams.set("standard", "erc20");
  url.searchParams.set("from_block", "0");
  url.searchParams.set("to_block", String(asOfBlock));
  url.searchParams.set("limit", "500");
  // clamp truncates from the older end
  url.searchParams.set("clamp", "true");
  if (cursor) url.searchParams.set("cursor", cursor);

  const res = await fetch(url, {
    headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
  });
  const page = await res.json();
  transfers.push(...page.data);
  cursor = page.next_cursor; // absent on the last page
} while (cursor);

Requête 3 : métadonnées de tokens et tokens:batch

Lisez un seul token avec GET /{chain}/tokens/{token} ; le chemin ne prend que {chain} et {token}, sans pagination. L'enveloppe de réponse est TokenEnvelope, et data est un Token :

ChampTypeDescription
addressstring (adresse)Adresse du contrat de token.
standardstringerc20, erc721 ou unknown.
namestring ou nullNom du token, ou null lorsqu'il est indisponible.
symbolstring ou nullSymbole du token, ou null lorsqu'il est indisponible.
decimalsinteger ou nullDécimales du token, de 0 à 255, ou null lorsqu'elles sont indisponibles.
total_supplystring ou nullOffre totale brute ; l'API n'applique pas d'ajustement selon les decimals. null lorsqu'elle est indisponible.
first_seen_blockinteger (int64)Hauteur de bloc où le token a été vu pour la première fois.
metadata_updated_atstring (horodatage)Heure UTC de la dernière mise à jour des métadonnées.
metadata_blockinteger (int64)Hauteur de bloc à laquelle les métadonnées ont été lues.
metadata_statusstringok, partial ou unavailable.
metadata_issuesobjectEnregistrements d'anomalies par champ indexés par name, symbol, decimals, total_supply, avec les valeurs reverted, no_data, invalid_encoding ou temporarily_unavailable.

Un {token} qui n'est pas une adresse valide de 20 octets renvoie 400 bad_request ; un {token} inconnu renvoie 404 not_found ; une {chain} inconnue renvoie 404 unknown_chain.

Le point de terminaison des soldes inclut déjà symbol et decimals lorsqu'ils sont disponibles, mais les deux peuvent valoir null. Pour compléter le nom et les décimales de chaque token dans un wallet, utilisez POST /{chain}/tokens:batch :

  • Le corps de la requête est {"addresses": [...]} avec au maximum 100 adresses par requête ; plus de 100 entrées, ou une entrée qui n'est pas une adresse valide de 20 octets, renvoie 400 bad_request (la requête échoue sur la première adresse invalide rencontrée).
  • Les adresses introuvables ne déclenchent pas d'erreur ; elles sont répertoriées dans data.missing, tandis que data.tokens contient uniquement les tokens dont les métadonnées ont été trouvées.
  • Les adresses dupliquées sont dédupliquées dans tokens et missing, chacune selon l'ordre de première apparition dans la requête.
export BLOCKVECTRA_API_KEY=rgw_your_api_key

# Token unique
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

# Lot : jusqu'à 100 adresses par requête
curl -s -X POST "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens:batch" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"addresses":["0x1111111111111111111111111111111111111111","0x2222222222222222222222222222222222222222"]}'

Ajuster les montants selon les décimales

Le champ de solde balance et le champ de transfert ERC-20 amount sont des entiers bruts restitués sous forme de chaînes décimales (UInt256String) ; l'offre totale total_supply d'un token est également un entier brut on-chain sans ajustement de decimals. Pour afficher une quantité lisible par un humain, divisez par les decimals de ce token.

  • decimals provient du champ symbol/decimals propre à l'élément de solde, ou de GET /{chain}/tokens/{token} et POST /{chain}/tokens:batch ; il peut valoir null.
  • Ces valeurs pouvant dépasser 2^53, n'effectuez pas l'opération arithmétique avec un nombre JSON : utilisez BigInt en TypeScript et Decimal en Python, en analysant la chaîne décimale telle quelle pour éviter toute perte de précision.
function toDisplayAmount(raw: string, decimals: number | null): string {
  if (decimals === null) return raw; // pas de métadonnées de décimales : conserver l'entier brut
  const value = BigInt(raw);
  const base = 10n ** BigInt(decimals);
  const whole = value / base;
  const fraction = (value % base)
    .toString()
    .padStart(decimals, "0")
    .replace(/0+$/, "");
  return fraction ? `${whole}.${fraction}` : whole.toString();
}

// balance.balance est une chaîne décimale brute ; decimals provient du même élément ou de tokens:batch.
const display = toDisplayAmount(balance.balance, balance.decimals);

Fraîcheur des données

Chaque réponse réussie ciblée sur une chaîne comporte meta :

  • as_of_block : le bloc le plus récent entièrement écrit de la chaîne. Les points de terminaison délimités par des blocs fournissent des données jusqu'à cette hauteur.
  • safe_block : un marqueur indiquant l'étiquette de bloc de consensus safe du nœud (null tant qu'il est inconnu). Jamais inférieur à finalized_block, et ne tronque, ne rejette ni ne retarde les réponses.
  • finalized_block : un marqueur indiquant l'étiquette de bloc de consensus finalized du nœud (null tant qu'il est inconnu). Il ne tronque, ne rejette ni ne retarde les réponses ; les clients décident du niveau de sécurité requis à partir du marqueur (comme le statut de confirmation).
  • coverage : "full" ou "partial". Les transferts d'adresses et points de terminaison similaires indiquent "partial" lorsque clamp a restreint la fenêtre servie, ou lorsque la fenêtre commence avant le premier bloc indexé de la chaîne.
  • refreshed_at : moment où les données sous-jacentes à la réponse ont été mises à jour pour la dernière fois (UTC). Peut valoir null : null signifie que l'heure de mise à jour des données est inconnue et qu'elles doivent être traitées comme périmées ; les points de terminaison basés sur des blocs renvoient toujours une valeur.
  • Il répète également chain, chain_slug et chain_external_id.

Un schéma courant : lisez meta.as_of_block dès la première réponse pour lire jusqu'au bloc indexé le plus récent, et vérifiez meta.safe_block / meta.finalized_block si vous souhaitez afficher le statut confirmé.

Estimation en CU pour le chargement d'une page

Chaque méthode est facturée selon son poids en CU, lu depuis l'API des forfaits de la plateforme :

Poids en CU par appel

MéthodeCU par appel
data.address_balances25
data.address_transfers25
data.tokens_batch10

Un chargement de page (estimé)

1 requête balances + 3 pages de transferts + 1 requête(s) tokens:batch, 5 appels au total, environ 110 CU. L'utilisation réelle dépend du nombre de pages et de tokens.

Pour les décisions de facturation et les réponses d'erreur non facturées, consultez les règles de facturation. Si vous n'avez pas besoin d'un historique de transferts indexé mais des logs des blocs les plus récents, lisez d'abord Données récentes de nœud vs historique indexé avant de décider de basculer vers eth_getLogs.

Prochaines étapes

Dernière mise à jour :

Sur cette page