Los endpoints de lista de Sui, incluido suix_queryTransactionBlocks, devuelven un sobre de página de { data, nextCursor, hasNextPage } en lugar de un offset y un recuento total. Los cursores son tokens opacos: se pasan de vuelta textualmente y nunca se construyen, decodifican ni avanzan aritméticamente. La paginación hacia delante avanza en orden ascendente, y descendingOrder invierte la dirección del recorrido, pero el cursor sigue codificando la posición. Como los resultados se leen en un checkpoint o epoch específico y los cursores pueden invalidarse al cruzar límites de prune, un recorrido largo debe persistir su último cursor, tolerar la invalidación con un reinicio acotado por checkpoint y deduplicar por digest de transacción. Este artículo construye un paginador ejecutable y reanudable, y muestra cómo demostrar la continuidad con una marca de agua de digest.
El sobre de página de suix_queryTransactionBlocks y su contrato de cursor
Los endpoints de lista JSON-RPC de Sui devuelven un sobre de página uniforme: un array de resultados en data, un nextCursor opaco y un booleano hasNextPage. La Referencia de la API JSON-RPC de Sui para suix_queryTransactionBlocks documenta esta forma, y el mismo contrato se aplica a suix_queryEvents y sui_getCheckpoints. Tu paginador debe tratar el sobre como la única fuente de verdad sobre dónde está el recorrido.
El cursor es opaco. La Documentación de Sui sobre paginación por cursor describe los cursores como tokens que deben devolverse textualmente; no debes analizarlos, sumarles nada ni sintetizar uno a partir de un digest o número de secuencia. Cualquier código que haga aritmética con un cursor depende de un detalle de implementación que puede cambiar sin previo aviso.
La paginación hacia delante usa orden ascendente por defecto. Establecer descendingOrder invierte la dirección del recorrido, pero el cursor sigue codificando la posición dentro de ese recorrido, por lo que debes mantener el mismo orden durante todo el recorrido. Mezclar direcciones a mitad del recorrido es una causa común de huecos y repeticiones.
data: la página de bloques de transacciones para esta solicitud.nextCursor: token opaco para la siguiente página; null cuando el recorrido se ha agotado.hasNextPage: si existe otra página; no lo infieras dedata.length.descendingOrder: invierte la dirección del recorrido; mantenlo constante para un recorrido dado.
Por qué la paginación por cursor reemplaza a la paginación por offset para bloques de transacciones
La paginación por offset solicita limit filas comenzando en offset. En un ledger activo, se añaden continuamente nuevos bloques de transacciones, por lo que la fila que estaba en el offset 1000 cuando empezaste puede estar en el offset 1005 cuando solicites la siguiente página. Ese desplazamiento produce tanto huecos (filas omitidas) como duplicados (filas vistas dos veces). La paginación por cursor evita esto porque el cursor ancla la siguiente lectura a una posición en el conjunto de resultados ordenado en lugar de a un recuento.
La paginación por offset también se degrada a medida que crece el offset: el backend debe saltar un número creciente de filas antes de devolver la página. La paginación por cursor permite que el backend reanude desde una posición indexada, que es la razón por la que es el patrón recomendado para la enumeración a escala de ledger. La contrapartida es que no puedes saltar a una página arbitraria ni calcular un recuento total solo a partir del sobre.
Para la enumeración de bloques de transacciones, la consecuencia práctica es que un recorrido reanudable es una máquina de estados: persistir el cursor, solicitar la siguiente página, añadir los resultados y repetir hasta que hasNextPage sea false. El centro de aprendizaje de OnFinality recopila patrones relacionados de RPC de Sui si quieres el contexto más amplio antes de construir este.
- Offset: posición por recuento; inestable bajo anexados concurrentes; se degrada con offsets grandes.
- Cursor: posición por token opaco; estable bajo anexados; sin acceso aleatorio ni recuento total.
- Para un recorrido de ledger completo, la paginación por cursor es la única opción segura para la continuidad.
Un recorrido hacia delante mínimo con suix_queryTransactionBlocks
Empieza con el bucle correcto más pequeño: solicita una página, añade data, lee nextCursor y detente cuando hasNextPage sea false. Este ejemplo usa el SDK de TypeScript de Sui contra un endpoint de Sui; sustituye tu propia URL de endpoint. La guía de RPC de Sui en RPC Assistant cubre la selección de endpoint y la disponibilidad de métodos si estás eligiendo un proveedor.
Observa que el bucle nunca inspecciona el cursor. Lo almacena y lo devuelve sin cambios. Ese es todo el contrato. Si te encuentras queriendo saber qué hay dentro del cursor, probablemente estés intentando resolver un problema que pertenece a tu propia lógica de marca de agua.
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
const client = new SuiClient({ url: getFullnodeUrl('mainnet') });
async function walkAll(filter = {}) {
const out = [];
let cursor = null;
let hasNextPage = true;
while (hasNextPage) {
const page = await client.queryTransactionBlocks({
filter,
cursor,
limit: 50,
order: 'ascending',
options: { showEffects: true },
});
out.push(...page.data);
cursor = page.nextCursor;
hasNextPage = page.hasNextPage;
}
return out;
}
walkAll({}).then((txs) => console.log('blocks:', txs.length));Filtrar por objeto de entrada, objeto modificado y tipo de transacción
suix_queryTransactionBlocks acepta un objeto de filtro que reduce el conjunto de resultados antes de la paginación. Las variantes de filtro documentadas incluyen InputObject, ChangedObject, FromAddress, ToAddress, FromAndToAddress, TransactionKind y MoveFunction. Filtrar en el servidor es tanto más rápido como más correcto que filtrar en el cliente después de un recorrido completo, porque el cursor entonces rastrea el conjunto de resultados filtrado en lugar del ledger sin filtrar.
Una sutileza: el cursor está vinculado al filtro que usaste. Si cambias el filtro a mitad del recorrido, el cursor ya no se refiere al mismo conjunto de resultados ordenado y la continuidad queda indefinida. Persiste el filtro junto con el cursor para que un recorrido reanudado use la consulta idéntica. Esta es también la razón por la que un reinicio acotado por checkpoint debe reaplicar el mismo filtro.
Para la indexación centrada en objetos, ChangedObject suele ser lo que quieres al rastrear el historial de mutaciones de un objeto, mientras que InputObject captura las transacciones que consumieron el objeto. La distinción importa para la deduplicación porque una sola transacción puede aparecer bajo múltiples filtros.
InputObject: transacciones que usaron el objeto como entrada.ChangedObject: transacciones que mutaron el objeto.TransactionKind: restringe a una transacción programable, prólogo de confirmación de consenso, etc.MoveFunction: restringe a llamadas de un package::module::function específico.
Persistir el cursor y el filtro como estado reanudable
Un recorrido reanudable necesita estado duradero: el último cursor, el filtro, el orden y una marca de agua. Almacénalos juntos para que un reinicio no pueda emparejar accidentalmente un cursor con un filtro diferente. Un pequeño registro JSON en un archivo, clave de Redis o fila de base de datos es suficiente. La página del servicio de API describe cómo OnFinality expone los endpoints de RPC de Sui si estás integrando esto en un pipeline alojado.
La marca de agua es la prueba de continuidad. Registra el último digest de transacción que añadiste, más el checkpoint o epoch en el que se leyó la página si la respuesta lo expone. Al reanudar, comparas el primer digest de la nueva página con la marca de agua: si coincide, el recorrido es continuo; si no, tienes un hueco o un retroceso y debes reconciliar.
Persiste después de cada página, no al final. Un fallo a mitad del recorrido debería perder como máximo una página de progreso, y la marca de agua te dice exactamente dónde estabas.
import fs from 'node:fs';
const STATE = './sui-walk-state.json';
function loadState() {
if (!fs.existsSync(STATE)) return null;
return JSON.parse(fs.readFileSync(STATE, 'utf8'));
}
function saveState(state) {
fs.writeFileSync(STATE, JSON.stringify(state, null, 2));
}
// state shape:
// {
// "cursor": "opaque-token-or-null",
// "filter": { "ChangedObject": "0xabc..." },
// "order": "ascending",
// "watermarkDigest": "...",
// "watermarkCheckpoint": "12345678",
// "seen": ["digest1", "digest2"]
// }Manejar la invalidación del cursor con un reinicio acotado por checkpoint
Los cursores pueden invalidarse. La documentación de Sui señala que los resultados se leen en un checkpoint o epoch específico y que los cursores pueden no sobrevivir a los límites de prune, por lo que un cursor reanudado puede ser rechazado o puede referirse silenciosamente a una posición diferente. La respuesta segura es un reinicio acotado por checkpoint: descarta el cursor inválido, elige un checkpoint de límite inferior de tu marca de agua y vuelve a recorrer hacia delante desde ahí, deduplicando contra los digests que ya almacenaste.
Un reinicio acotado por checkpoint no es un reescaneo completo. Reinicias desde el último checkpoint conocido como bueno, que está acotado por cuánto se retrasa tu marca de agua respecto al límite de prune. Si tu marca de agua es reciente, la ventana de reinicio es pequeña. Si está obsoleta, la ventana crece, y por eso importa la persistencia frecuente.
Detecta la invalidación capturando el error de RPC y validando el primer digest de la página reanudada contra la marca de agua. Una discrepancia es una señal para reiniciar, no para añadir. El artículo sobre tiempo de espera de RPC de Sui cubre el caso relacionado en el que la solicitud falla por razones de transporte en lugar de por razones del cursor.
- Ante error de cursor: descarta el cursor, conserva el filtro y el orden, reinicia desde el checkpoint de la marca de agua.
- Ante discrepancia de digest: trátalo como un hueco o retroceso; reinicia desde el checkpoint de la marca de agua.
- Ante invalidación repetida: reduce el tamaño de página y persiste con más frecuencia para reducir la ventana de reinicio.
Deduplicar por digest de transacción y demostrar continuidad con una marca de agua
Las lecturas cercanas a una reorganización pueden resurgir un digest: una transacción que estaba en una página que ya consumiste puede aparecer de nuevo tras un reinicio o una reorganización. Deduplica por digest de transacción, que es la identidad estable de un bloque de transacciones. Mantén un conjunto acotado de digests vistos recientemente, o un conjunto persistente si el recorrido abarca reinicios.
La marca de agua es la prueba de continuidad. Después de cada página, establece watermarkDigest con el último digest añadido y watermarkCheckpoint con el checkpoint en el que se leyó la página. Al reanudar, verifica que el primer digest de la nueva página sea el sucesor de la marca de agua en el mismo orden. Si no lo es, tienes un hueco y debes reiniciar desde el checkpoint de la marca de agua.
Esta combinación —cursor opaco para la posición, conjunto de digests para la deduplicación, marca de agua para la continuidad— es lo que hace que el recorrido esté libre de huecos y duplicados sin depender de ningún interno del cursor.
async function resumableWalk(client, filter, order = 'ascending') {
let state = loadState() ?? {
cursor: null,
filter,
order,
watermarkDigest: null,
watermarkCheckpoint: null,
seen: [],
};
const seen = new Set(state.seen);
const appended = [];
let duplicates = 0;
let hasNextPage = true;
while (hasNextPage) {
let page;
try {
page = await client.queryTransactionBlocks({
filter: state.filter,
cursor: state.cursor,
limit: 50,
order: state.order,
options: { showEffects: true },
});
} catch (err) {
// Cursor invalidated: restart from the watermark checkpoint.
state.cursor = null;
saveState(state);
continue;
}
for (const tx of page.data) {
const digest = tx.digest;
if (seen.has(digest)) {
duplicates += 1;
continue;
}
seen.add(digest);
appended.push(tx);
state.watermarkDigest = digest;
}
state.cursor = page.nextCursor;
state.seen = [...seen].slice(-10000);
saveState(state);
hasNextPage = page.hasNextPage;
}
return { appended, duplicates };
}Tabla de resultados: medir páginas, transacciones, duplicados y tiempo total
Mide tu propio endpoint en lugar de confiar en cualquier número publicado. Ejecuta el paginador contra un filtro fijo y una ventana de checkpoint fija, y registra los contadores a continuación. El objetivo es confirmar que los duplicados descartados son pocos y estables, y que el tiempo total escala aproximadamente de forma lineal con las páginas recorridas.
Rellena esta tabla para cada ejecución. Compara ejecuciones con diferentes tamaños de página para ver la contrapartida entre el número de solicitudes y la latencia por solicitud. Si los duplicados se disparan, tu ventana de reinicio es demasiado grande o tu marca de agua está obsoleta.
- Páginas recorridas: número de llamadas exitosas a
suix_queryTransactionBlocks. - Transacciones devueltas: total de
data.lengthsumado entre páginas. - Duplicados descartados: digests ya presentes en el conjunto de vistos.
- Wall ms: tiempo transcurrido para todo el recorrido.
- Reinicios: número de reinicios por invalidación de cursor activados.
| Run | Filter | Page size | Pages walked | Txs returned | Duplicates dropped | Restarts | Wall ms |
|-----|--------|-----------|--------------|--------------|--------------------|----------|---------|
| 1 | | 50 | | | | | |
| 2 | | 100 | | | | | |
| 3 | | 200 | | | | | |Solución de problemas de huecos, repeticiones y errores de cursor
Síntoma: el mismo digest aparece en dos páginas consecutivas. Causa: el recorrido cambió de filtro u orden a mitad de flujo, o un reinicio releyó una página sin deduplicación. Solución: mantén el filtro y el orden constantes, y deduplica siempre por digest.
Síntoma: falta un digest entre dos páginas. Causa: se avanzó un cursor mediante aritmética, o un reinicio se saltó el checkpoint de la marca de agua. Solución: nunca construyas cursores; reinicia desde el checkpoint de la marca de agua y vuelve a recorrer hacia delante.
Síntoma: el RPC devuelve un error de cursor tras una pausa larga. Causa: el cursor cruzó un límite de prune. Solución: captura el error, descarta el cursor y reinicia desde el checkpoint de la marca de agua. Si esto ocurre a menudo, reduce el tamaño de página y persiste con más frecuencia. El artículo sobre límites de tasa y cómputo de RPC de Sui cubre el caso relacionado en el que la limitación de tasa, y no el estado del cursor, es el modo de fallo.
- Repeticiones: comprueba la estabilidad de filtro/orden y la deduplicación.
- Huecos: comprueba si hay aritmética de cursor y lógica de reinicio.
- Errores de cursor: comprueba los límites de prune y la frescura de la marca de agua.
- Limitación de tasa: comprueba los límites de tasa y el backoff antes de culpar al cursor.
Limitaciones: prune, límites de epoch y contrapartidas de ordenación
La paginación por cursor no es una instantánea. Los resultados se leen en un checkpoint o epoch específico, y los cursores pueden invalidarse al cruzar límites de prune. Un recorrido que abarca un límite de epoch puede necesitar reconciliarse con un reinicio acotado por checkpoint incluso si no se genera ningún error. Planifícalo en lugar de tratarlo como una excepción.
El orden descendente es útil para seguir la actividad reciente, pero no sustituye a una instantánea estable. Si necesitas una vista consistente, acota el recorrido a un rango de checkpoints y acepta que el rango puede releerse tras un reinicio. El artículo sobre streaming de checkpoints de Sui: servicio de ledger gRPC cubre la alternativa de streaming cuando necesitas entrega continua en lugar de un recorrido acotado.
Por último, la paginación por cursor no te da un recuento total ni acceso aleatorio. Si tu producto necesita un recuento o un número de página, calcúlalo por separado a partir de un índice que controles, no del sobre de RPC.
- Sin garantía de instantánea: los resultados se leen en un checkpoint o epoch.
- Los límites de prune pueden invalidar cursores; reinicia desde un checkpoint de marca de agua.
- Sin recuento total ni acceso aleatorio solo a partir del sobre.
- El orden descendente invierte el recorrido pero no crea una instantánea estable.
Próximos pasos: paginación de eventos, efectos de transacción y elección de proveedor
La enumeración de bloques de transacciones es uno de tres patrones relacionados de RPC de Sui. El mecanismo hermano es la paginación de eventos, cubierta en Consultar eventos de Sui por RPC: filtros y paginación por cursor, que usa el mismo sobre y contrato de cursor. Si necesitas analizar qué cambió una transacción, consulta Efectos de transacción de RPC de Sui: objectChanges y balanceChanges.
Para entrega continua en lugar de un recorrido acotado, el streaming de checkpoints encaja mejor. Para la selección de endpoint y la planificación de capacidad, revisa Límites de tasa y cómputo de RPC de Sui y Precios de RPC. La página de redes de Sui lista las redes de Sui que OnFinality soporta, y la guía de RPC de Sui cubre la disponibilidad de métodos por endpoint.
Un siguiente paso práctico es ejecutar la tabla de resultados anterior contra dos proveedores y comparar los duplicados descartados y los reinicios. Esa comparación, y no un benchmark publicado, es el número que importa para tu carga de trabajo.
- Paginación de eventos: mismo sobre, mismo contrato de cursor.
- Efectos de transacción: analiza objectChanges y balanceChanges después de la enumeración.
- Streaming de checkpoints: entrega continua en lugar de un recorrido acotado.
- Elección de proveedor: mide duplicados y reinicios en tu propio endpoint.