XDR
Stellar almacena y comunica datos del ledger, transacciones, resultados, historial y mensajes en un formato binario llamado Representación de Datos Externos (XDR). XDR se define en RFC4506. XDR está optimizado para el rendimiento de la red, pero no es legible para los humanos. Horizon y los SDK de Stellar convierten los XDR en formatos más amigables.
.X files
Las estructuras de datos en XDR se especifican en archivos .x
. Estos archivos solo contienen definiciones de estructuras de datos, sin operaciones ni código ejecutable. Los archivos .x
para las estructuras XDR utilizadas en la Red Stellar están disponibles en GitHub.
Stellar XDR se puede codificar en JSON utilizando el esquema XDR-JSON.
Más Sobre XDR
XDR es similar a herramientas como Protocol Buffers o Thrift. XDR proporciona varias características importantes:
- Es muy compacto, por lo que puede ser transmitido rápidamente y almacenado con un espacio en disco mínimo.
- Los datos codificados en XDR se almacenan de manera fiable y predecible. Los campos siempre están en el mismo orden, lo que hace que firmar y verificar mensajes XDR criptográficamente sea sencillo.
- Las definiciones XDR incluyen descripciones detalladas de tipos de datos y estructuras, lo que no es posible en formatos más simples como JSON, TOML o YAML.
Parseo de XDR
Dado que XDR es un formato binario y no es tan conocido como formatos más simples como JSON, todos los SDK de Stellar incluyen herramientas para parsear XDR y lo harán automáticamente al recuperar datos.
Además, el servidor API de Horizon generalmente expone las partes más importantes de los datos XDR en JSON, por lo que son más fáciles de parsear si no estás utilizando un SDK. Los datos XDR aún están incluidos (codificados como una cadena base64) dentro del JSON en caso de que necesites acceso directo a ellos.
Analizando las estructuras XDR
Dado que el formato XDR es un fundamento fundamental de la Red Stellar, a menudo encontramos necesario analizar estas estructuras de datos binarios en bruto para que los clientes interactúen con cosas como transacciones y entradas de autorización. Puede ser difícil de entender debido a su estructura, pero esta sección buscará iluminarte sobre las formas de interactuar con ella en un puñado de lenguajes populares. El esquema del Protocolo está definido en el stellar/stellar-xdr
repositorio, aunque contiene mucho más de lo que necesitaremos para nuestros propósitos.
Formas Comunes de XDR
En el Protocolo Stellar, XDR toma un puñado de formas diferentes que son importantes de entender y distinguir:
Primitivas Básicas
Estas son las más fáciles de entender para cualquiera que esté familiarizado con un lenguaje de programación e incluyen cosas como enteros, cadenas y matrices de bytes. Estas son bastante sencillas de interactuar, aunque las cosas pueden complicarse un poco cuando tienes alias.
Por ejemplo, tienes el alias Uint64
en el Protocolo Stellar (definido Stellar-types.x), definido para la legibilidad en lugar del valor nativo "unsigned hyper" de XDR. Puedes tratarlo exactamente como esperarías:
import { xdr } from "@stellar/stellar-sdk";
const u64 = new xdr.Uint64(12345678);
Algunas variaciones de lenguaje te permitirán instanciarlo de varias maneras. Por ejemplo, dado que los enteros de 64 bits son un poco... extraños en JavaScript, también puedes inicializarlo con un BigInt
o incluso un tipo de cadena:
import { xdr } from "@stellar/stellar-sdk";
let u64 = new xdr.Uint64("1_000_000_000_000_000");
u64 = new xdr.Uint64(1_000_000_000_000_000n);
Puedes ver el conjunto completo de opciones para construir un Uint64
en la definición para UnsignedHyper
en la definición de TypeScript aqu í: curr.d.ts. De hecho, este archivo debería ser tu guía principal para navegar por el XDR si estás usando JavaScript.
Otros lenguajes, como Python (ver la documentación de stellar-sdk
en uint64.html), admiten enteros de tamaño arbitrario por defecto, por lo que no hay tales variaciones:
import xdr from stellar_sdk;
u64 = xdr.Uint64(1_000_000_000_000_000)
Como resultado, muchos de los elementos básicos pueden ser inicializados de manera "intuitiva" para tu respectivo lenguaje.
Uniones
Aquí es donde las cosas se complican un poco más. Un tipo de unión es un tipo genérico que alterna entre diferentes "brazos", cada uno de los cuales es un tipo interno diferente. Al analizar un tipo de unión XDR, es importante determinar qué brazo se está utilizando antes de intentar analizar lo que hay dentro.
Tomemos un SCAddress
, por ejemplo. A partir de la definición del tipo, podemos ver que es ya sea una cuenta (es decir, SC_ADDRESS_TYPE_ACCOUNT
) o el hash de un contrato (SC_ADDRESS_TYPE_CONTRACT
). Justo encima de esa definición, puedes ver que son constantes del enum SCAddressType
definidas como 0 o 1, respectivamente.
En general, las uniones siguen un patrón particular: cambia según el tipo y igualan el valor del enum correspondiente. Así que cuando tienes una de estas uniones, necesitas averiguar cuál es:
- TypeScript
- Python
import { xdr } from "@stellar/stellar-sdk";
import Buffer from "buffer";
// suppose `scAddr` is an instance of `xdr.ScAddress` we're parsing
switch (scAddr.switch()) {
case xdr.ScAddressType.scAddressTypeAccount().value:
console.log("The account is:", scAddr.accountId());
break;
case xdr.ScAddressType.scAddressTypeContract().value:
console.log("The contract is:", scAddr.contractId());
break;
default:
throw new Error(`Unexpected address type: ${scAddr.switch()}`);
}
import SCAddressType, SCAddress from stellar_sdk.xdr.sc_address_type
if scAddr.type == SCAddressType.SC_ADDRESS_TYPE_ACCOUNT:
print("The account is:", scAddr.account_id);
break
elif scAddr.type == SCAddressType.SC_ADDRESS_TYPE_CONTRACT:
print("The contract is:", scAddr.contract_id);
break
else:
raise Exception(f"Unexpected address type: {scAddr.type}")
Puedes ver las referencias para esto en la documentación (Python y JavaScript, respectivamente).
Vale la pena notar que podrías simplemente usar las constantes de enum definidas directamente, por ejemplo.
import { xdr } from "@stellar/stellar-sdk";
import Buffer from "buffer";
// suppose `scAddr` is an instance of `xdr.ScAddress` we're parsing
switch (scAddr.switch().value) {
case 0:
console.log("The account is:", scAddr.accountId());
break;
case 1:
console.log("The contract is:", scAddr.contractId());
break;
default:
throw new Error(`Unexpected address type: ${scAddr.switch()}`);
}
Sin embargo, esto generalmente es más propenso a errores y más difícil de leer. También puedes referirte a ellos por su nombre (.name
en JS y str(...)
en Python): esto es marginalmente mejor para la legibilidad pero peor para el rendimiento ya que estás haciendo comparaciones de cadenas. Es muy útil para registros, renderización para el usuario, u otras depuraciones, sin embargo.
Las uniones aparecen en muchos lugares a lo largo del XDR y pueden ser solo un brazo o un montón de brazos. Por ejemplo, el smart contract value ScVal
tiene 22 valores posibles ya que es la representación universal para un valor dentro de un contrato inteligente. Puedes ver todos ellos siendo inspeccionados en la utilidad de JavaScript scValToNative
.
Recuerda, XDR es el nivel más bajo de comunicación en la red Stellar. En términos generales, hay abstracciones de alto nivel que deberían ayudar con estas estructuras. Por ejemplo, la rutina de análisis anterior es precisamente el propósito de las abstracciones Address.fromScAddress
y Address.from_xdr_sc_address
en los SDK de JavaScript y Python, respectivamente. Esto simplifica enormemente tu código:
- TypeScript
- Python
import { Address } from "@stellar/stellar-sdk";
// suppose `scAddr` is an instance of `xdr.ScAddress` we're parsing
const address = Address.fromScAddress(scAddr);
import Address from stellar_sdk;
# suppose `scAddr` is an instance of `xdr.ScAddress` we're parsing
addr = Address.from_xdr_sc_address(scAddr)
Siempre trata de encontrar una abstracción de nivel superior antes de profundizar en la estructura en bruto.
Estructuras
Como en cualquier lenguaje similar a C, una estructura es un objeto empaquetado que contiene un montón de campos de tipos arbitrarios. Las estructuras pueden contener a su vez estructuras, así que asegúrate de recorrer todo el árbol de campos cuando estés construyendo una.
Tomemos, por ejemplo, la estructura ContractEvent
. Su definición es un poco complicada, así que desglosemosla:
- Un
ExtensionPoint
es una forma de extender el protocolo manteniendo la compatibilidad binaria hacia atrás.
Básicamente es un espacio vacío en el que puedes agregar variaciones en el futuro. La estructura SorobanTransactionMeta
es un gran ejemplo de esto en acción: cuando la estructura se agregó inicialmente al protocolo, solo contenía los detalles de una invocación (eventos y un valor de retorno). Más tarde, necesitábamos más detalles sobre la invocación en los metadatos resultantes, así que SorobanTransactionMetaExt
fue inyectado en el ExtensionPoint
con una V1
variante que contiene metadatos sobre las tarifas cobradas como resultado de la invocación.
- El
contractID
debería ser autoexplicativo: es el hash del ID de contrato que causó este evento. Es un puntero, sin embargo, porque no todos los eventos están relacionados con un contrato específico. Algunos pueden venir del sistema, diagnósticos, etc.
No debería sorprender que el campo se almacene como un Hash
: aunque en la capa del ecosistema nos referimos a los contratos por su representación "strkey" o representación de cadena (en la forma C...
), estas son realmente representaciones "más amigables" de un hash SHA-256 en bruto. Esto es común en la capa del protocolo: siempre se usará la representación más simple y compacta. Generalmente hay herramientas SDK para hacer la traducción de estos a tipos más amigables para el usuario (como se demostró en el ejemplo a continuación).
-
El
ContractType
es esencialmente una forma de diferenciar cuáles de los campos en la estructura deberías esperar que estén presentes y no vale la pena elaborarlo. -
Finalmente, tenemos el curioso campo de unión
v
.
A menudo verás uniones en estructuras y viceversa. Si tienes una idea de cómo se atraviesan cada uno de estos individualmente, puedes manejarlos entrelazados entre sí. Como demostración de cómo se hace esto con un SDK, incluso diseñado específicamente para esta estructura, podemos echar un vistazo al código fuente del método auxiliar humanizeEvents
en el SDK de JavaScript. Específicamente, veremos los detalles de la subrutina extractEvent
, replicada aquí por conveniencia:
function extractEvent(event) {
return {
...(typeof event.contractId === "function" &&
event.contractId() != null && {
contractId: StrKey.encodeContract(event.contractId()),
}),
type: event.type().name,
topics: event
.body()
.value()
.topics()
.map((t) => scValToNative(t)),
data: scValToNative(event.body().value().data()),
};
}
En este ejemplo corto y conciso podemos ver muchas de las sutilezas que hemos discutido en esta guía en acción:
- Al diferenciar si el
contractId
devuelve un valor o no (es un puntero, recuerda?), decidimos si queremos convertir el hash en bruto a un ID de contrato compatible con el ecosistema. - Accedemos al campo
.name
del tipo de evento, que lo convierte en una cadena legible por humanos en lugar de una estructura opaca, tal como discutimos en Uniones, anteriormente. - Inspeccionamos profundamente el miembro
body
para extraer los campostopics
ydata
por separado, convirtiendo cada uno de esos valores opacosScVal
en equivalentes amigables para JavaScript.
En la última parte, "abusamos" del hecho de que solo hay una variación del cuerpo de la unión que discutimos: solo existe la versión inicial (un brazo con v == 0
), así que podemos usar el accessor .value()
para darnos directamente el valor subyacente en lugar de pasar por la engorrosa tarea de diferenciar los diferentes casos. Llamar a .v0()
habría sido equivalente, pero la ventaja aquí es que si se introduce una variación v == 1
que también tiene un array de topics
y un campo data
(es decir, agrega campos adicionales en lugar de cambiar la estructura v0), este código seguirá funcionando!
Hay muchos casos en los que los diferentes brazos de la unión comparten estructura, y .value()
te permite aprovechar eso.
Esta visión general debería darte una base sólida para entender cómo inspeccionar XDR en tu respectivo SDK para profundizar en los campos que te interesan. Los detalles, por supuesto, dependerán del lenguaje, pero si comienzas desde la base—es decir, los archivos en bruto .x mismos—obtendrás una comprensión de la estructura misma y deberías poder acceder a esa misma estructura directamente en tu lenguaje de elección, como hemos señalado aquí.