Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Guías de red y protocolo12 min de lectura

Leer saldos y metadatos de tokens ERC-20 mediante eth_call

Una guía práctica para leer saldos, decimales, símbolo y totalSupply de ERC-20 con eth_call, incluyendo codificación ABI, decodificación y solución de problemas.

TL;DR

El saldo de un token ERC-20 no se almacena en la billetera; vive dentro del contrato del token como un mapeo de dirección a uint256. Leerlo requiere una eth_call a la función balanceOf(address) del contrato del token, con datos de llamada construidos a partir del selector de función de 4 bytes y argumentos rellenados a 32 bytes. El mismo patrón lee decimals(), symbol(), name() y totalSupply(). Esta guía explica la codificación ABI, muestra ejemplos ejecutables en Node.js y proporciona un manual de solución de problemas para fallos comunes como saldos cero y direcciones de contrato incorrectas.

Por qué los saldos ERC-20 viven en el contrato del token

El saldo de un token ERC-20 no es una propiedad de una billetera. La billetera es una cuenta de propiedad externa (EOA) o un contrato que no almacena estado específico del token. En cambio, el contrato del token mantiene un mapeo de dirección a uint256, como se define en EIP-20. Cuando llamas a balanceOf(holder), el contrato busca en ese mapeo y devuelve el valor.

Este diseño significa que leer el saldo de un token requiere una eth_call al contrato del token, no una consulta sobre la billetera. La dirección de la billetera es solo un argumento de la función. Lo mismo se aplica a metadatos como decimals, symbol, name y totalSupply: todos son almacenados y devueltos por el contrato del token.

Debido a que el saldo es estado del contrato, puede cambiar entre bloques. La etiqueta de bloque que pasas a eth_call determina qué estado se utiliza. Si omites la etiqueta de bloque, el nodo normalmente usa el último bloque, pero este comportamiento está documentado y varía según el proveedor. Especifica siempre una etiqueta de bloque cuando necesites resultados reproducibles.

  • Los saldos de tokens se almacenan en el almacenamiento del contrato del token, no en la billetera.
  • balanceOf(address) devuelve un uint256 del mapeo del contrato.
  • Las funciones de metadatos (decimals, symbol, name, totalSupply) también son lecturas del contrato.
  • La etiqueta de bloque controla qué instantánea de estado se utiliza.

La forma de la solicitud eth_call para lecturas de tokens

El método eth_call ejecuta una llamada de mensaje de solo lectura contra el estado del nodo y devuelve los datos de retorno. No crea una transacción, no consume gas de tu cuenta y no persiste nada. El objeto de solicitud tiene un campo to para la dirección del contrato del token y un campo data para los datos de llamada codificados en ABI. Se puede incluir un parámetro de bloque opcional como segundo argumento.

Según la especificación JSON-RPC de Ethereum, eth_call toma un objeto de transacción y un número o etiqueta de bloque. El objeto de transacción no debe incluir un campo from para una lectura pura, aunque algunos proveedores lo aceptan. El campo data son los datos de llamada codificados en hexadecimal.

Una solicitud mínima se ve así: {"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0xTokenContract","data":"0x70a08231..."},"latest"],"id":1}. La respuesta contiene un campo result con los datos de retorno codificados en hexadecimal.

const response = await fetch('https://api.onfinality.io/eth', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    jsonrpc: '2.0',
    method: 'eth_call',
    params: [
      {
        to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
        data: '0x70a08231000000000000000000000000d8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
      },
      'latest'
    ],
    id: 1
  })
});
const json = await response.json();
console.log(json.result); // 0x... (32-byte hex)

Codificación ABI: selector de función y argumentos rellenados

Los datos de llamada para una lectura ERC-20 se construyen a partir de dos partes: un selector de función de 4 bytes y los argumentos codificados en ABI. El selector son los primeros 4 bytes de keccak256 de la firma de la función, como balanceOf(address). Para balanceOf(address), el selector es 0x70a08231. Para decimals(), es 0x313ce567. Para symbol(), es 0x95d89b41. Para name(), es 0x06fdde03. Para totalSupply(), es 0x18160ddd.

Cada argumento se rellena a la izquierda hasta 32 bytes. Una dirección tiene 20 bytes, por lo que se rellena con 12 bytes de ceros a la izquierda. Por ejemplo, la dirección 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 se convierte en 0x000000000000000000000000d8dA6BF26964aF9D7eEd9e03E53415D37aA96045. Los datos de llamada completos son el selector concatenado con el argumento rellenado.

La especificación ABI de Solidity define esta codificación. Puedes construirla a mano o usar una biblioteca como ethers.js, web3.js o viem. Las bibliotecas son menos propensas a errores, pero entender la codificación ayuda al depurar respuestas sin procesar.

  • Selector de balanceOf(address): 0x70a08231
  • Selector de decimals(): 0x313ce567
  • Selector de symbol(): 0x95d89b41
  • Selector de name(): 0x06fdde03
  • Selector de totalSupply(): 0x18160ddd

Decodificación de datos de retorno: uint256 y cadenas dinámicas

Los datos de retorno de eth_call están codificados en hexadecimal. Para balanceOf, decimals y totalSupply, el retorno es una sola palabra de 32 bytes que se puede decodificar como uint256. Para decimals, el valor es un uint8 almacenado en una palabra de 32 bytes, por lo que lees el último byte o analizas toda la palabra como un entero. Para balanceOf y totalSupply, toda la palabra de 32 bytes es el valor entero.

Para symbol y name, el retorno es una cadena dinámica codificada en ABI. La primera palabra de 32 bytes es un desplazamiento a los datos de la cadena, la siguiente palabra de 32 bytes en ese desplazamiento es la longitud, y los bytes siguientes son la cadena UTF-8. Decodificar esto manualmente requiere leer el desplazamiento, luego la longitud, y luego cortar los bytes de la cadena.

Un error común es tratar todo el retorno como un uint256 para symbol o name. Eso produce un número enorme, no una cadena. Verifica siempre la firma de la función y decodifica en consecuencia.

function decodeUint256(hex) {
  return BigInt(hex);
}

function decodeString(hex) {
  const data = hex.slice(2); // remove 0x
  const offset = parseInt(data.slice(0, 64), 16) * 2;
  const length = parseInt(data.slice(offset, offset + 64), 16) * 2;
  const stringHex = data.slice(offset + 64, offset + 64 + length);
  return Buffer.from(stringHex, 'hex').toString('utf8');
}

// Example usage:
// const rawBalance = '0x00000000000000000000000000000000000000000000000000000000000f4240';
// console.log(decodeUint256(rawBalance)); // 1000000n
// const rawSymbol = '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000045553444300000000000000000000000000000000000000000000000000000000';
// console.log(decodeString(rawSymbol)); // 'USDC'

Leer decimals y convertir saldos sin procesar

La función decimals() devuelve el número de decimales que usa el token. La mayoría de los tokens usan 18, pero esto no está garantizado. USDC usa 6, por ejemplo. Para convertir un saldo sin procesar a una cantidad legible por humanos, divide el saldo sin procesar por 10^decimals. Para un token con 6 decimales, un saldo sin procesar de 1000000 equivale a 1.0 token.

Llama siempre a decimals() antes de mostrar un saldo. Asumir 18 decimales para un token como USDC mostrará un saldo que está desviado por un factor de 10^12. La conversión es una división simple, pero debe usar el valor correcto de decimales.

Si decimals() revierte o devuelve un valor inesperado, el contrato puede no implementar completamente el estándar ERC-20. Algunos tokens devuelven un valor fijo u omiten la función por completo. En tales casos, es posible que debas recurrir a un valor conocido o manejar el error con elegancia.

  • decimals() devuelve uint8, típicamente 18 pero no siempre.
  • Saldo legible por humanos = saldo sin procesar / 10^decimals.
  • USDC usa 6 decimales; DAI usa 18.
  • Obtén siempre decimals antes de formatear.

Ejemplo ejecutable en Node.js: saldo, decimales, símbolo y conversión

El siguiente script de Node.js usa JSON-RPC sin procesar sobre fetch para leer el saldo de un titular, los decimales del token y el símbolo del token. Luego convierte el saldo sin procesar a una cantidad legible por humanos y decodifica el retorno de cadena dinámica para symbol. Reemplaza la URL de RPC, el contrato del token y la dirección del titular con tus propios valores.

Este ejemplo usa el endpoint de Ethereum de OnFinality como marcador de posición. Puedes usar cualquier endpoint RPC de Ethereum, incluido tu propio nodo o un proveedor. El script demuestra el flujo completo: codificar datos de llamada, enviar eth_call, decodificar datos de retorno y formatear el resultado.

const RPC_URL = 'https://api.onfinality.io/eth';
const TOKEN = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // USDC
const HOLDER = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045';

async function rpc(method, params) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', method, params, id: 1 })
  });
  const json = await res.json();
  if (json.error) throw new Error(json.error.message);
  return json.result;
}

function encodeBalanceOf(address) {
  const selector = '70a08231';
  const padded = address.toLowerCase().replace('0x', '').padStart(64, '0');
  return '0x' + selector + padded;
}

function decodeUint256(hex) {
  return BigInt(hex);
}

function decodeString(hex) {
  const data = hex.slice(2);
  const offset = parseInt(data.slice(0, 64), 16) * 2;
  const length = parseInt(data.slice(offset, offset + 64), 16) * 2;
  const stringHex = data.slice(offset + 64, offset + 64 + length);
  return Buffer.from(stringHex, 'hex').toString('utf8');
}

async function main() {
  const balanceHex = await rpc('eth_call', [
    { to: TOKEN, data: encodeBalanceOf(HOLDER) },
    'latest'
  ]);
  const rawBalance = decodeUint256(balanceHex);

  const decimalsHex = await rpc('eth_call', [
    { to: TOKEN, data: '0x313ce567' },
    'latest'
  ]);
  const decimals = Number(decodeUint256(decimalsHex));

  const symbolHex = await rpc('eth_call', [
    { to: TOKEN, data: '0x95d89b41' },
    'latest'
  ]);
  const symbol = decodeString(symbolHex);

  const human = Number(rawBalance) / 10 ** decimals;
  console.log(`Token: ${symbol}`);
  console.log(`Raw balance: ${rawBalance}`);
  console.log(`Decimals: ${decimals}`);
  console.log(`Human-readable balance: ${human}`);
}

main().catch(console.error);

Tabla de resultados reproducibles para tu propio endpoint

Para verificar el comportamiento de tu endpoint RPC, completa la siguiente tabla con valores que midas tú mismo. Usa un contrato de token conocido y una dirección de titular conocida. Registra el ID de cadena, el retorno sin procesar de balanceOf, el valor de decimals, el saldo legible por humanos, el símbolo y el suministro total. Repite la medición en diferentes etiquetas de bloque para ver cómo la etiqueta de bloque afecta el resultado.

Esta tabla es una plantilla. No confíes en números prellenados de este artículo; mide contra tu propio endpoint. El objetivo es confirmar que tu endpoint devuelve datos consistentes y que tu lógica de decodificación es correcta.

  • Contrato del token: [tu dirección de token]
  • ID de cadena: [tu ID de cadena]
  • balanceOf sin procesar: [hex o decimal]
  • Decimales: [entero]
  • Saldo legible por humanos: [sin procesar / 10^decimals]
  • Símbolo: [cadena]
  • Suministro total: [totalSupply sin procesar]

Fallo común: balanceOf devuelve cero

Una llamada balanceOf que devuelve cero es el problema más común. Por lo general, significa una de tres cosas: la dirección del contrato del token es incorrecta, el ID de cadena es incorrecto, o estás leyendo una dirección de proxy que no implementa balanceOf directamente. Si la dirección del contrato es incorrecta, eth_call puede devolver 0x o revertir. Si la cadena es incorrecta, la dirección puede no existir en esa cadena.

Para diagnosticar, primero verifica que el contrato existe en la cadena que estás consultando usando eth_getCode. Un resultado de bytecode no vacío confirma que se ha desplegado un contrato. Luego llama a symbol() o name() para confirmar que el contrato es un token ERC-20. Si symbol() revierte, el contrato puede no cumplir con ERC-20 o puede ser un proxy que requiere una interfaz diferente.

Para contratos proxy, la dirección de implementación se almacena en una ranura de almacenamiento específica. Puedes leer esa ranura con eth_getStorageAt y luego llamar a la implementación. Sin embargo, muchos proxies reenvían llamadas de forma transparente, por lo que balanceOf puede seguir funcionando. Si no lo hace, revisa el ABI o la documentación del proxy.

  • Dirección de contrato de token incorrecta: verifica con eth_getCode.
  • ID de cadena incorrecto: asegúrate de que el token existe en la cadena que consultas.
  • Dirección de proxy: verifica si el proxy reenvía llamadas o requiere la dirección de implementación.
  • Token no estándar: algunos tokens no implementan balanceOf como se espera.

Limitaciones y compensaciones de las lecturas de tokens con eth_call

Los contratos de tokens varían. Algunos son proxies, algunos devuelven tipos no estándar, y los tokens de rebase o con comisión por transferencia hacen que balanceOf sea un objetivo móvil. Un token de rebase cambia los saldos sin transferencias, por lo que el valor que lees puede no coincidir con la expectativa del usuario. Los tokens con comisión por transferencia deducen una comisión en la transferencia, por lo que el saldo del destinatario puede ser menor que la cantidad enviada.

La etiqueta de bloque afecta el resultado. Si consultas 'latest', el saldo refleja el estado en el último bloque conocido por tu endpoint. Si consultas un número de bloque específico, el saldo refleja ese estado histórico. Para resultados reproducibles, especifica siempre un número de bloque.

eth_call refleja el estado del endpoint que consultas. Diferentes proveedores pueden estar en diferentes alturas de bloque o pueden tener diferentes políticas de poda de estado. Si necesitas resultados consistentes entre proveedores, compara los números de bloque y usa la misma etiqueta de bloque.

  • Los proxies y tokens no estándar pueden romper lecturas simples.
  • Los tokens de rebase y con comisión por transferencia hacen que los saldos sean dinámicos.
  • La etiqueta de bloque determina la instantánea de estado.
  • El estado del endpoint puede variar según el proveedor.

Lista de verificación para la solución de problemas de lecturas de tokens con eth_call

Cuando falla una lectura de token con eth_call, recorre una lista de verificación. Primero, confirma que el endpoint RPC es accesible y devuelve una respuesta válida para un método simple como eth_blockNumber. Segundo, verifica la dirección del contrato del token y el ID de cadena. Tercero, comprueba que los datos de llamada están codificados correctamente: el selector debe coincidir con la firma de la función y los argumentos deben estar rellenados a 32 bytes.

Si la llamada revierte, decodifica el motivo de reversión usando las técnicas de Decodificar motivos de reversión y errores personalizados de Ethereum. Si los datos de retorno están vacíos, el contrato puede no implementar la función. Si los datos de retorno son inesperados, revisa la lógica de decodificación ABI.

Para depuración a nivel de almacenamiento, puedes inspeccionar la ranura de almacenamiento sin procesar de un saldo usando eth_getStorageAt y el diseño de almacenamiento de EVM. Esto es avanzado pero útil cuando balanceOf devuelve cero inesperadamente. Además, verifica el bytecode del contrato con eth_getCode: Leer el bytecode de un contrato para asegurarte de que es un contrato, no una EOA.

  • Verifica el endpoint RPC con eth_blockNumber.
  • Verifica el contrato del token y el ID de cadena.
  • Confirma la codificación de los datos de llamada: selector y relleno.
  • Decodifica los motivos de reversión si la llamada falla.
  • Inspecciona las ranuras de almacenamiento para depuración avanzada.
  • Verifica el bytecode del contrato con eth_getCode.

Próximos pasos: integrar lecturas de tokens en tu aplicación

Una vez que puedas leer saldos y metadatos de ERC-20 de manera confiable, integra estas llamadas en tu aplicación. Usa una biblioteca como ethers.js o viem para manejar la codificación y decodificación ABI, pero conserva los ejemplos de JSON-RPC sin procesar para depuración. Almacena en caché los valores de decimals y symbol, ya que rara vez cambian, pero obtén siempre los saldos actualizados con una etiqueta de bloque.

Para producción, considera usar un proveedor RPC dedicado con tiempo de actividad confiable y estado consistente. OnFinality ofrece nodos RPC de Ethereum y un servicio de API que pueden soportar tus cargas de trabajo de lectura de tokens. Revisa precios de RPC para elegir un plan que se ajuste a tu volumen de solicitudes.

Para profundizar, explora simulación de anulación de estado de eth_call para lecturas de estado hipotéticas, y la guía de nodos RPC de Ethereum para mejores prácticas de operación de nodos. El centro de aprendizaje de OnFinality tiene más guías sobre métodos JSON-RPC de Ethereum.

  • Usa bibliotecas para codificar pero entiende los datos de llamada sin procesar.
  • Almacena en caché los metadatos; obtén los saldos con una etiqueta de bloque.
  • Elige un proveedor RPC con estado consistente.
  • Explora las anulaciones de estado para simulación.
  • Lee la guía de nodos RPC de Ethereum para consejos operativos.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar