Smart Contracts
Los smart contracts permiten gestionar el estado del blockchain a través de código desplegado en el blockchain. Viven en el blockchain de forma permanente, se ejecutan en la ULVM, la máquina virtual WebAssembly de ULedger, y cada invocación es en sí misma una transaction en la cadena.
Los smart contracts se escriben en WebAssembly y se compilan en archivos .wat. Si aún no se ha escrito uno, se recomienda comenzar con la documentación de ULVM. Cubre los conceptos básicos sobre cómo escribir smart contracts.
Despliegue de un contrato
El despliegue es un POST multiparte. Se envía el archivo .wat junto con los parámetros de la transaction. La billetera con la que se realiza el despliegue debe estar ya registrada en el blockchain.
import fs from 'fs';
import FormData from 'form-data';
const form = new FormData();
form.append('walletAddress', 'your-wallet-address');
form.append('blockchainId', '08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2');
form.append('from', 'your-wallet-address');
form.append('payload', 'erc20-token-contract');
form.append('file', fs.createReadStream('./contracts/erc20.wat'));
const res = await fetch('https://your-tms-url/api/v1/smartcontracts/deploy', {
method: 'POST',
headers: {
'X-AccessKey': 'your-entity-access-key',
...form.getHeaders(),
},
body: form,
});
const deployResult = await res.json();
console.log('Deploy transaction ID:', deployResult.transactionId);
El despliegue crea una transaction en la cadena. Esa dirección de destino es también la dirección del contrato. Se debe guardar para invocar el contrato más adelante. La transaction debería aparecer en un nuevo bloque inmediatamente después del despliegue.
Invocación de un contrato
Se llama a cualquier función exportada por nombre junto con sus argumentos. El contrato se ejecuta en la ULVM y cada invocación es una transaction en la cadena. Se puede verificar el estado de la transaction, el bloque en el que se registró y el estado del contrato tras la invocación.
const res = await fetch('https://your-tms-url/api/v1/smartcontracts/invoke/payload', {
method: 'POST',
headers: {
'X-AccessKey': 'your-entity-access-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
walletAddress: 'your-wallet-address',
blockchainId: '08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2',
contractAddress: deployResult.transactionId,
functionName: 'mint',
arguments: [
{ type: 'string', value: 'your-wallet-address' },
{ type: 'int32', value: '1000' },
],
gasLimit: 0,
}),
});
const invokeResult = await res.json();
console.log('Invocation result:', invokeResult);
Tipos de argumento compatibles: int32, int64, float32, float64, bool, string, bytes.
:::tip Definir gasLimit explícitamente y leer el resultado desde outputResult
gasLimit: 0 no significa ilimitado, se eleva al mínimo de 100000, que no alcanza para un contrato
que hace muchas llamadas de almacenamiento. El rango válido es 100000–10000000; se recomienda aumentarlo para
contratos que escriben en el almacenamiento de forma repetida.
El valor de retorno de un contrato llega en la transaction confirmada como outputResult, base64 de
[type:1][length:4][value]; para un i32 eso es tag 2, longitud 4, big-endian. El campo output
solo indica SUCCESS.
// returns 1000 from an i32 return value
const raw = Buffer.from(tx.outputResult, 'base64'); // 02 00000004 000003e8
const value = raw.readInt32BE(5);
Cuando una llamada falla, output es TRANSACTION_ERROR y outputResult contiene una cadena de error
simple como contract aborted: .... Se debe verificar que output === 'SUCCESS' antes de decodificar, porque una
cadena de error no es base64 y decodificarla produce un número arbitrario.
:::
:::info Paso de argumentos
Los números se pasan como cadenas ({ "type": "int32", "value": "1000" }). No se debe pasar un argumento de
cadena vacía: una cadena de longitud cero no escribe ningún byte en la memoria del guest, por lo que el
contrato lee lo que ocupe esa dirección. Se debe exponer una función separada en su lugar.
:::
Verificación del estado del contrato
Los smart contracts pueden almacenar estado. Después de invocar, se comprueba qué cambió a través del TMS:
:::info Un 404 aquí significa "sin estado", no "sin contrato"
El endpoint de estado responde 404 para un contrato que existe pero nunca ha escrito nada, por lo que un
404 por sí solo es ambiguo. Se debe verificar primero el contrato:
curl "https://your-node-url/blockchains/{blockchainId}/contracts/{contractAddress}"
Si eso devuelve el contrato, un 404 desde /state significa que aún no tiene estado. El cuerpo del
error es texto plano en lugar de JSON.
:::
const res = await fetch(
`https://your-tms-url/api/v1/smartcontracts/${blockchainId}/${contractAddress}/state`,
{
headers: {
'X-AccessKey': 'your-entity-access-key',
'Accept': 'application/json',
},
}
);
const state = await res.json();
console.log('Estado del contrato:', state);
// p. ej. { "contract:version": "1", "counter": "42" }
Para rastrear una invocación específica y ver exactamente qué instrucciones se ejecutaron y cuánto gas consumieron:
const res = await fetch(
`https://your-tms-url/api/v1/smartcontracts/${blockchainId}/${contractAddress}/trace/${transactionId}`,
{
headers: {
'X-AccessKey': 'your-entity-access-key',
'Accept': 'application/json',
},
}
);
const trace = await res.json();
console.log('Gas utilizado:', trace.gasUsed);
console.log('Salida:', trace.output);
Actualización de un contrato
La dirección del contrato permanece igual entre actualizaciones mientras solo cambia el código. Cada versión anterior queda preservada en la cadena.
import fs from 'fs';
import FormData from 'form-data';
const form = new FormData();
form.append('walletAddress', 'your-wallet-address');
form.append('blockchainId', '08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2');
form.append('contractAddress', deployResult.transactionId);
form.append('upgradeReason', 'added burn function');
form.append('file', fs.createReadStream('./contracts/erc20-v2.wat'));
const upgradeRes = await fetch('https://your-tms-url/api/v1/smartcontracts/upgrade', {
method: 'POST',
headers: {
'X-AccessKey': 'your-entity-access-key',
...form.getHeaders(),
},
body: form,
});
console.log('Upgrade status:', upgradeRes.status);
Tras la actualización, se invocan las nuevas funciones de la misma manera, con la misma dirección de contrato y los nuevos nombres de función disponibles:
const res = await fetch('https://your-tms-url/api/v1/smartcontracts/invoke/payload', {
method: 'POST',
headers: {
'X-AccessKey': 'your-entity-access-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
walletAddress: 'your-wallet-address',
blockchainId: '08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2',
contractAddress: deployResult.transactionId,
functionName: 'burn',
arguments: [
{ type: 'string', value: 'your-wallet-address' },
{ type: 'int32', value: '100' },
],
gasLimit: 0,
}),
});
Desplegar una vez, invocar las veces que sea necesario, actualizar cuando la lógica cambie. El contrato siempre estará ahí, siempre en la cadena, siempre rastreable.
Revertir un contrato
Si una actualización introduce un error o rompe algo en producción, se puede revertir a cualquier versión anterior. Todas las versiones se conservan en la cadena, y una reversión restaura el bytecode activo a la versión seleccionada.
const res = await fetch('https://your-tms-url/api/v1/smartcontracts/rollback', {
method: 'POST',
headers: {
'X-AccessKey': 'your-entity-access-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
walletAddress: 'your-wallet-address',
blockchainId: '08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2',
contractAddress: 'your-contract-address',
targetVersion: 1,
rollbackReason: 'Error crítico encontrado en V2',
}),
});
const result = await res.json();
console.log('Transaction de rollback:', result.transactionId);
La dirección del contrato no cambia. Tras la reversión, las invocaciones ejecutan nuevamente el bytecode de V1. Se puede actualizar de nuevo en cualquier momento, y el historial de versiones sigue creciendo hacia adelante en la cadena.