Trabajar con especificaciones de contrato en Java, Python y PHP
Introducción
Los contratos inteligentes de Soroban son herramientas poderosas para construir aplicaciones descentralizadas en la red Stellar. Para interactuar con estos contratos de manera efectiva, es crucial entender sus especificaciones y cómo utilizarlas en tu lenguaje de programación de elección.
Una especificación de contrato típica (espec) incluye:
- Tipos de datos utilizados por el contrato
- Definiciones de función con sus entradas y salidas
- Tipos de error que el contrato puede devolver
Estos detalles guían cómo interactúas con el contrato, independientemente del lenguaje de programación que estés utilizando.
Requisitos previos
Antes de sumergirte en las interacciones con el contrato, asegúrate de tener lo siguiente:
- CLI de Stellar (
stellar
) instalado - Un SDK compatible con Soroban para tu lenguaje de programación. Consulta la lista de SDKs disponibles para encontrar uno que se adapte a tus necesidades
- Acceso a un servidor RPC de Stellar (local o en una red de pruebas)
Para esta guía, nos centraremos en los SDK de Java, Python, y PHP como referencia, pero los conceptos también pueden aplicarse a otros lenguajes.
¿Qué son las especificaciones de contrato?
Una especificación de contrato es como un ABI (Interfaz Binaria de Aplicación) en Ethereum. Es una descripción estandarizada de la interfaz de un contrato inteligente, típicamente en formato JSON. Define las funciones del contrato, estructuras de datos, eventos y errores de una manera que las aplicaciones externas pueden entender y usar.
Esta especificación actúa como un puente crucial entre el contrato inteligente y las aplicaciones cliente, permitiendo que interactúen sin necesidad de conocer los detalles internos de implementación del contrato.
Generación de especificaciones de contrato
La CLI de Stellar proporciona un comando para generar una especificación de contrato a partir del código fuente de un contrato. Este proceso es fácil pero requiere que tengas el binario Wasm del contrato.
A veces, es posible que no tengas acceso al código fuente del contrato o la capacidad de compilarlo. En tales casos, debes usar el comando stellar contract fetch
para descargar el binario Wasm del contrato y generar la especificación.
Finalmente, usamos el comando stellar bindings
para generar la especificación del contrato a partir del binario Wasm.
Obteniendo el binario del contrato
stellar contract fetch --network-passphrase 'Test SDF Network ; September 2015' --rpc-url https://soroban-testnet.stellar.org --id CONTRACT_ID --out-file contract.wasm
Generando la especificación del contrato a partir de Wasm
stellar contract bindings json --wasm contract.wasm > abi.json
Entendiendo la especificación del contrato
La especificación ABI (Interfaz Binaria de Aplicación) para contratos inteligentes Stellar incluye varios componentes clave que definen cómo interactuar con el contrato. Examinemos estos en detalle con ejemplos:
-
Funciones: Las funciones se definen con su nombre, entradas y salidas. Representan los métodos llamables del contrato. Pueden usarse para escribir datos en el contrato y leer datos del contrato.
Ejemplo:
{
"type": "function",
"name": "mint",
"inputs": [
{
"name": "contract",
"value": { "type": "address" }
},
{
"name": "minter",
"value": { "type": "address" }
},
{
"name": "to",
"value": { "type": "address" }
},
{
"name": "amount",
"value": { "type": "i128" }
}
],
"outputs": [
{
"type": "result",
"value": { "type": "tuple", "elements": [] },
"error": { "type": "error" }
}
]
}Esto define una función
mint
que toma cuatro parámetros y devuelve ya sea una tupla vacía o un error. Fíjate en el tipo de cada parámetro:address
para las direcciones de cuentas Stellar,i128
para enteros de 128 bits, etc. -
Estructuras: Las estructuras definen tipos de datos complejos con múltiples campos.
Ejemplo:
{
"type": "struct",
"name": "ClaimableBalance",
"fields": [
{
"name": "amount",
"value": { "type": "i128" }
},
{
"name": "claimants",
"value": {
"type": "vec",
"element": { "type": "address" }
}
},
{
"name": "time_bound",
"value": {
"type": "custom",
"name": "TimeBound"
}
},
{
"name": "token",
"value": { "type": "address" }
}
]
}Esto define una estructura
ClaimableBalance
con cuatro campos. -
Uniones: Las uniones representan variables que pueden ser de varios tipos.
Ejemplo:
{
"type": "union",
"name": "DataKey",
"cases": [
{
"name": "Init",
"values": []
},
{
"name": "Balance",
"values": []
}
]
}Esto define una unión
DataKey
que puede serInit
oBalance
. -
Tipos personalizados: Los tipos personalizados se refieren a otros tipos definidos en la ABI.
Ejemplo:
{
"name": "time_bound",
"value": {
"type": "custom",
"name": "TimeBound"
}
}Esto se refiere a un tipo
TimeBound
personalizado definido en otro lugar de la ABI. -
Tipos de vector: Los vectores representan matrices de un tipo específico.
Ejemplo:
{
"name": "claimants",
"value": {
"type": "vec",
"element": { "type": "address" }
}
}Esto define un vector de direcciones.
-
Tipos primitivos: Estos incluyen tipos básicos como
i128
(entero de 128 bits),u64
(entero sin signo de 64 bits),address
, etc.Ejemplo:
{
"name": "amount",
"value": { "type": "i128" }
}
Estas especificaciones son cruciales para codificar y decodificar datos al interactuar con el contrato. Por ejemplo:
- Al llamar a la función
mint
, debes proporcionar cuatro parámetros: tres direcciones y un entero de 128 bits. - Si una función devuelve un
ClaimableBalance
, esperarías recibir una estructura con una cantidad (i128), un vector de direcciones (claimants), un objeto TimeBound, y una dirección (token). - Si una función podría devolver un
Error
, lo más probable es que falle en la simulación y no necesitarás decodificar el resultado.
Tipos en Soroban
Antes de sumergirnos en la interacción con los contratos inteligentes de Stellar, es importante notar que Soroban tiene su propio conjunto de tipos que se utilizan para interactuar con los contratos según lo descrito en esta guía. Aquí hay algunos de los tipos comunes:
u32
: Entero sin signo de 32 bitsu64
: Entero sin signo de 64 bitsi32
: Entero con signo de 32 bitsi64
: Entero con signo de 64 bitsu128
: Entero sin signo de 128 bitsi128
: Entero con signo de 128 bitsbool
: Booleanostring
: Cadena codificada en UTF-8vec
: Arreglo de longitud variableaddress
: Dirección de cuenta Stellarmap
: Mapa clave-valorsymbol
: Una cadena pequeña utilizada principalmente para nombres de función y claves de mapa
En esta guía y los SDKs, estos tipos se representan como ScU32
, ScU64
, ScI32
, ScI64
, ScU128
, ScI128
, ScBool
, ScString
, ScVec
, ScAddress
, ScMap
, y ScSymbol
respectivamente.
Cada otro tipo complejo puede derivarse utilizando estos tipos básicos, pero estos tipos no se traducen realmente a valores en los lenguajes de programación. Los SDKs de Stellar proporcionan clases de ayuda para trabajar con estos tipos.
Trabajando con tipos nativos de Soroban
Una de las tareas más comunes al trabajar con contratos inteligentes de Stellar es convertir entre tipos de contrato inteligente de Stellar y tipos nativos en tu lenguaje de programación. En esta guía, revisaremos algunas conversiones comunes y mostraremos cómo pueden usarse para invocar contratos con la ayuda de la especificación del contrato.
En la mayoría de los SDKs, se usa la clase o función ScVal
para convertir entre tipos Soroban y tipos nativos.
El bloque de código JSON muestra la especificación del contrato, mientras que los bloques de código en Rust muestran el contrato para cada ejemplo.
1. Invocar una función de contrato sin parámetros
Usaremos la función increment
del contrato de incremento de ejemplo para ejemplificar esto. La función increment
no toma parámetros y aumenta el contador en 1.
En este escenario, no es necesario realizar conversiones y pasar el valor null
como argumentos del contrato es suficiente en la mayoría de los SDKs.
- Rust
- JSON
- Python
- Java
- PHP
#[contractimpl]
impl IncrementContract {
/// Increment increments an internal counter, and returns the value.
pub fn increment(env: Env) -> u32 {
// Get the current count.
let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0.
log!(&env, "count: {}", count);
// Increment the count.
count += 1;
// Save the count.
env.storage().instance().set(&COUNTER, &count);
env.storage().instance().extend_ttl(50, 100);
// Return the count to the caller.
count
}
}
[
{
"type": "function",
"doc": "Increment increments an internal counter, and returns the value.",
"name": "increment",
"inputs": [],
"outputs": [
{
"type": "u32"
}
]
}
]
# pip install --upgrade stellar-sdk
from stellar_sdk import SorobanServer, soroban_rpc, Account, Asset, Keypair, Network, TransactionBuilder
def send_transaction() -> soroban_rpc.SendTransactionResponse:
server = SorobanServer(server_url='https://soroban-testnet.stellar.org', client=None)
root_keypair = Keypair.from_secret(
"SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
)
root_account = server.load_account("GBSBL6FBPX5UHKL4AZCPUU6PXKUBYMKRUN3L4YQ4V2CCWSE7YMN2HYPB")
contract_id = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"
transaction = (
TransactionBuilder(
source_account=root_account,
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
base_fee=100,
)
.append_invoke_contract_function_op(contract_id,"increment")
# mark this transaction as valid only for the next 30 seconds
.set_timeout(30)
.build()
)
transaction.sign(root_keypair)
response = server.send_transaction(transaction)
return response
response = send_transaction()
print("status", response.status)
print("hash:", response.hash)
print("status:", response.status)
print("errorResultXdr:", response.error_result_xdr)
// implementation 'network.lightsail:stellar-sdk:0.44.0'
import org.stellar.sdk.AccountConverter;
import org.stellar.sdk.InvokeHostFunctionOperation;
import org.stellar.sdk.KeyPair;
import org.stellar.sdk.Network;
import org.stellar.sdk.SorobanServer;
import org.stellar.sdk.Transaction;
import org.stellar.sdk.TransactionBuilder;
import org.stellar.sdk.TransactionBuilderAccount;
import org.stellar.sdk.responses.sorobanrpc.SendTransactionResponse;
public class SendTransactionExample {
public static void main(String[] args) {
SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org");
try {
TransactionBuilderAccount account = server.getAccount("GBSBL6FBPX5UHKL4AZCPUU6PXKUBYMKRUN3L4YQ4V2CCWSE7YMN2HYPB");
KeyPair sourceKeyPair = KeyPair.fromSecretSeed("SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
String contractId = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE";
InvokeHostFunctionOperation operation = InvokeHostFunctionOperation.invokeContractFunctionOperationBuilder(contractId, "increment", null).build();
// Build the transaction
Transaction transaction = new TransactionBuilder(AccountConverter.enableMuxed(), account, Network.TESTNET)
.addOperation(operation)
.build();
// Sign the transaction
transaction.sign(sourceKeyPair);
// Send the transaction using the SorobanServer
SendTransactionResponse response = server.sendTransaction(transaction);
System.out.println(response.getStatus());
System.out.println(response.getHash());
System.out.println(response.getLatestLedger());
System.out.println(response.getLatestLedgerCloseTime());
} catch (Exception e) {
System.err.println("An error has occurred:");
e.printStackTrace();
}
}
}
<?php
namespace App\Models;
use Soneso\StellarSDK\TransactionBuilder;
use Soneso\StellarSDK\Soroban\SorobanServer;
use Soneso\StellarSDK\SEP\Derivation\Mnemonic;
use Soneso\StellarSDK\Crypto\KeyPair;
use Soneso\StellarSDK\Network;
use Soneso\StellarSDK\InvokeContractHostFunction;
use Soneso\StellarSDK\InvokeHostFunctionOperationBuilder;
use Soneso\StellarSDK\Soroban\Requests\SimulateTransactionRequest;
class Invoker
{
public function invoker2()
{
$server = new SorobanServer("https://soroban-testnet.stellar.org");
$accountASeed = "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
$accountAKeyPair = Keypair::fromSeed($accountASeed);
$accountAId = "GBWIPGMM27M7O43NIHS6MXMVBDDI6WUYXWCJX7FL7DBA6B4ITXBC6JPN";
$accountA = $server->getAccount($accountAId);
$network = Network::testnet();
$contractId = "CAMN24E6KNIXQBYPJJ4K7XRCUUJUMYSJRCSLZ2WOO6WJTSKIXAZWNYHK";
$invokeContractHostFunction = new InvokeContractHostFunction($contractId, "increment", null);
$builder = new InvokeHostFunctionOperationBuilder($invokeContractHostFunction);
$op = $builder->build();
$transaction = (new TransactionBuilder($accountA))
->addOperation($op)->build();
$request = new SimulateTransactionRequest($transaction);
$simulateResponse = $server->simulateTransaction($request);
$transaction->setSorobanTransactionData($simulateResponse->getTransactionData());
$transaction->addResourceFee($simulateResponse->minResourceFee);
$transaction->sign($accountAKeyPair, $network);
$server->sendTransaction($transaction);
}
}
Los ejemplos subsiguientes mostrarán bloques de código solo para usar la especificación del contrato para reducir la redundancia.
2. Invocar una función de contrato con uno o más parámetros
Generalmente, esto implica pasar un array nativo (no un ScVec) de parámetros a la función del contrato.
Usaremos la función hello
del contrato Hello World de ejemplo para ejemplificar esto.
En este escenario, necesitamos convertir el parámetro de cadena a un tipo ScString
antes de pasarlo al contrato. En este escenario, necesitamos convertir el parámetro de cadena a un tipo ScString
antes de pasarlo al contrato.
Este proceso es conveniente utilizando la clase o función ScVal
en la mayoría de los SDKs.
- Rust
- JSON
- Python
- Java
- PHP
#[contract]
pub struct HelloContract;
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: String) -> Vec<String> {
vec![&env, String::from_str(&env, "Hello"), to]
}
}
[
{
"type": "function",
"doc": "",
"name": "hello",
"inputs": [
{
"doc": "",
"name": "to",
"value": {
"type": "string"
}
}
],
"outputs": [
{
"type": "vec",
"element": {
"type": "string"
}
}
]
}
]
from stellar_sdk import Keypair, Network, SorobanServer, TransactionBuilder, scval
.....
tx = (
TransactionBuilder(source, network_passphrase, base_fee=100)
.set_timeout(300)
.append_invoke_contract_function_op(
contract_id=contract_id,
function_name="hello",
parameters=[
scval.to_string("John"),
]
).build())
import org.stellar.sdk.scval.Scv;
import org.stellar.sdk.xdr.SCVal;
// .....
List<SCVal> contractArgs = new ArrayList<SCVal>();
contractArgs.add(Scv.toString("John"));
InvokeHostFunctionOperation operation = InvokeHostFunctionOperation
.invokeContractFunctionOperationBuilder(contractId, "hello", contractArgs).build();
TransactionBuilder transaction = new TransactionBuilder(source, Network.TESTNET);
Transaction tx = transaction.addOperation(operation).build();
$arg = \Soneso\StellarSDK\Xdr\XdrSCVal::forString("John");
$invokeContractHostFunction = new InvokeContractHostFunction($contractId, "hello", [$arg]);
$builder = new InvokeHostFunctionOperationBuilder($invokeContractHostFunction);
$op = $builder->build();
$transaction = (new TransactionBuilder($accountA))
->addOperation($op)->build();
3. Obteniendo respuestas de los contratos
Los datos devueltos de los contratos también están en formato ScVal
y necesitan ser convertidos a tipos nativos en tu lenguaje de programación.
Todavía usaremos la función hello
del contrato Hello World de ejemplo para ejemplificar esto.
En este escenario, necesitamos convertir el valor devuelto de un ScVec
de tipo ScString
a un array
de string
antes de hacer uso de él. En este escenario, necesitamos convertir el valor devuelto de un ScVec
de tipo ScString
a un array
de string
antes de hacer uso de él.
Pasos:
- Extraer un
ScVec
del valor de retorno - Extraer cada
ScString
delScVec
- Convertir cada
ScString
a una cadena nativa
Este proceso es conveniente usando la clase o función ScVal
en la mayoría de los SDKs.
Preferiblemente, para recuperar este valor, necesitamos usar el método RPC getTransaction
utilizando el hash de respuesta de la transacción que invocó la función del contrato.
- Rust
- JSON
- Python
- Java
- PHP
#[contract]
pub struct HelloContract;
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: String) -> Vec<String> {
vec![&env, String::from_str(&env, "Hello"), to]
}
}
[
{
"type": "function",
"doc": "",
"name": "hello",
"inputs": [
{
"doc": "",
"name": "to",
"value": {
"type": "string"
}
}
],
"outputs": [
{
"type": "vec",
"element": {
"type": "string"
}
}
]
}
]
from stellar_sdk import SorobanServer, soroban_rpc
from stellar_sdk import xdr as stellar_xdr
from stellar_sdk.soroban_rpc import GetTransactionStatus
def get_transaction(hash: str) -> soroban_rpc.GetTransactionResponse:
server = SorobanServer(server_url='https://soroban-testnet.stellar.org', client=None)
tx = server.get_transaction(hash)
return tx
get_transaction_data = get_transaction("7e47c6ba2ebe53e156bc50c48e34302d49c91c04c465e8cd2b8a25219c2c8121")
if get_transaction_data.status == GetTransactionStatus.SUCCESS:
transaction_meta = stellar_xdr.TransactionMeta.from_xdr(
get_transaction_data.result_meta_xdr
)
result = transaction_meta.v3.soroban_meta.return_value
output = []
for x in result.vec.sc_vec:
decoded_string = x.str.sc_string.decode()
output.append(decoded_string)
print(f"transaction result: {output}")
else:
print(f"Transaction failed: {get_transaction_data.result_xdr}")
import org.stellar.sdk.responses.sorobanrpc.GetTransactionResponse;
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.xdr.TransactionMeta;
public static void main(String[] args) {
SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org");
try {
GetTransactionResponse tx = server
.getTransaction("7e47c6ba2ebe53e156bc50c48e34302d49c91c04c465e8cd2b8a25219c2c8121");
if (tx.getStatus() == GetTransactionResponse.GetTransactionStatus.SUCCESS) {
List<String> output = new ArrayList<String>();
String base64Xdr = tx.getResultMetaXdr();
// convert the string to a result
SCVal[] result = TransactionMeta.fromXdrBase64(base64Xdr).getV3()
.getSorobanMeta().getReturnValue().getVec()
.getSCVec();
for (SCVal x : result) {
output.add(x.getStr().getSCString().toString());
}
System.out.println("transaction result: " + output.toString());
} else {
System.out.println("Transaction failed: " + tx.getStatus());
}
} catch (Exception e) {
System.err.println("An error has occurred:");
e.printStackTrace();
}
}
<?php
use Soneso\StellarSDK\Soroban\Responses\GetTransactionResponse;
use Soneso\StellarSDK\Soroban\SorobanServer;
class Test {
public function getTx()
{
$txhash = "7e47c6ba2ebe53e156bc50c48e34302d49c91c04c465e8cd2b8a25219c2c8121";
$server = new SorobanServer("https://soroban-testnet.stellar.org");
$statusResponse = $server->getTransaction($txhash);
$status = $statusResponse->status;
$resultArr = [];
if ($status == GetTransactionResponse::STATUS_FAILED) {
print ("Transaction failed: " . $statusResponse->error . PHP_EOL);
} else if ($status == GetTransactionResponse::STATUS_SUCCESS) {
$resultValue = $statusResponse->getResultValue();
$resVec = $resultValue->vec;
foreach ($resVec as $strVal) {
$resultArr[] = $strVal->str;
}
print_r($resultArr);
}
return $resultArr;
}
}
Trabajando con tipos de datos complejos
Como se describe en esta guía, hay algunas otras variantes de estructuras de datos soportadas por Soroban. Ellas son
Struct
con campos nombradosStruct
con campos no nombradosEnum
(variantes unitarias y de tupla)Enum
(variantes enteras)
Veremos cómo estas variantes se traducen a la especificación y cómo construirlas en los diferentes SDKs.
Struct con campos nombrados
Las estructuras con valores nombrados cuando se convierten a ABI o especificación se representan como un ScMap
donde cada valor tiene la clave en ScSymbol
y el valor en el tipo subyacente.
- Rust
- JSON
- Python
- Java
- PHP
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct State {
pub count: u32,
pub last_incr: u32,
}
[
{
"type": "struct",
"doc": "",
"name": "State",
"fields": [
{
"doc": "",
"name": "count",
"value": {
"type": "u32"
}
},
{
"doc": "",
"name": "last_incr",
"value": {
"type": "u32"
}
}
]
}
]
from stellar_sdk import scval
scval.to_map(
{
scval.to_symbol("count"): scval.to_u32(0),
scval.to_symbol("last_incr"): scval.to_u32(0),
}
)
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.scval.Scv;
import java.util.LinkedHashMap;
LinkedHashMap<SCVal, SCVal> map = new LinkedHashMap<SCVal, SCVal>();
map.put(Scv.toSymbol("count"), Scv.toUint32(0));
map.put(Scv.toSymbol("last_incr"), Scv.toUint32(0));
SCVal val = Scv.toMap(map);
<?php
use Soneso\StellarSDK\Xdr\XdrSCVal;
XdrSCVal::forMap(
[
XdrSCVal::forSymbol("count") => XdrSCVal::forU32(0),
XdrSCVal::forSymbol("last_incr") => XdrSCVal::forU32(0),
]
);
Struct con campos no nombrados
Las estructuras con valores no nombrados cuando se convierten a ABI o especificación se representan como un ScVal
donde cada valor tiene el tipo subyacente.
- Rust
- JSON
- Python
- Java
- PHP
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct State(pub u32, pub u32);
[
{
"type": "struct",
"doc": "",
"name": "State",
"fields": [
{
"doc": "",
"value": {
"type": "u32"
}
},
{
"doc": "",
"value": {
"type": "u32"
}
}
]
}
]
from stellar_sdk import scval
scval.to_vec(
[
scval.to_uint32(0),
scval.to_uint32(0),
]
)
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.scval.Scv;
import java.util.LinkedHashMap;
List<SCVal> vec = new ArrayList<SCVal>();
vec.add(Scv.toUint32(0));
vec.add(Scv.toUint32(0));
SCVal val = Scv.toVec(vec);
<?php
use Soneso\StellarSDK\Xdr\XdrSCVal;
XdrSCVal::forVec(
[
XdrSCVal::forU32(0),
XdrSCVal::forU32(0),
]
);
Enum (variantes unitarias y de tupla)
Los enums generalmente se representan con ScVec
, sus tipos unitarios se representan como ScSymbol
y sus variantes de tupla se representan como los tipos subyacentes.
- Rust
- JSON
- Python
- Java
- PHP
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Enum {
A,
B(u32),
}
[
{
"type": "union",
"doc": "",
"name": "Enum",
"cases": [
{
"doc": "",
"name": "A",
"values": []
},
{
"doc": "",
"name": "B",
"values": [
{
"type": "u32"
}
]
}
]
}
]
from stellar_sdk import scval
scval.to_vec(
[
scval.to_symbol("A"),
scval.to_map(
{
scval.to_symbol("B"): scval.to_uint32(0),
}
),
]
)
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.scval.Scv;
import java.util.LinkedHashMap;
List<SCVal> vec = new ArrayList<SCVal>();
vec.add(Scv.toSymbol("A"));
LinkedHashMap<SCVal, SCVal> map = new LinkedHashMap<SCVal, SCVal>();
map.put(Scv.toSymbol("B"), Scv.toUint32(0));
vec.add(Scv.toMap(map));
SCVal val = Scv.toVec(vec);
<?php
use Soneso\StellarSDK\Xdr\XdrSCVal;
XdrSCVal::forVec(
[
XdrSCVal::forSymbol("A"),
XdrSCVal::forMap(
[
XdrSCVal::forSymbol("B") => XdrSCVal::forU32(0),
]
),
]
);
Enum (variantes enteras)
Los enums generalmente se representan con ScVec
, la variante entera no tiene claves, así que es solo un ScVec
del tipo subyacente.
- Rust
- JSON
- Python
- Java
- PHP
#[contracttype]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum Enum {
A = 1,
B = 2,
}
[
{
"type": "struct",
"doc": "",
"name": "Enum",
"fields": [
{
"doc": "",
"name": "A",
"type": "u32"
},
{
"doc": "",
"name": "B",
"type": "u32"
}
]
}
]
from stellar_sdk import scval
scval.to_vec(
[
scval.to_uint32(0),
scval.to_uint32(0),
]
)
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.scval.Scv;
import java.util.LinkedHashMap;
List<SCVal> vec = new ArrayList<SCVal>();
vec.add(Scv.toUint32(0));
vec.add(Scv.toUint32(0));
SCVal val = Scv.toVec(vec);
<?php
use Soneso\StellarSDK\Xdr\XdrSCVal;
XdrSCVal::forVec(
[
XdrSCVal::forU32(0),
XdrSCVal::forU32(0),
]
);
Un ejemplo complejo
Usaremos el ejemplo del contrato de bloqueo de tiempo para mostrar cómo interactuar con un contrato que tiene tipos de datos complejos.
Este ejemplo utiliza una estructura TimeBound
que tiene un enum TimeBoundKind
como uno de sus campos, que son parámetros para la función deposit
. Este ejemplo combina la mayoría de los conceptos que hemos discutido hasta ahora.
- Rust
- JSON
- Python
#[derive(Clone)]
#[contracttype]
pub enum TimeBoundKind {
Before,
After,
}
#[derive(Clone)]
#[contracttype]
pub struct TimeBound {
pub kind: TimeBoundKind,
pub timestamp: u64,
}
#[contracttype]
#[contractimpl]
impl ClaimableBalanceContract {
pub fn deposit(
env: Env,
from: Address,
token: Address,
amount: i128,
claimants: Vec<Address>,
time_bound: TimeBound,
) {}
}
[
{
"type": "union",
"doc": "",
"name": "TimeBoundKind",
"cases": [
{
"doc": "",
"name": "Before",
"values": []
},
{
"doc": "",
"name": "After",
"values": []
}
]
},
{
"type": "struct",
"doc": "",
"name": "TimeBound",
"fields": [
{
"doc": "",
"name": "kind",
"value": {
"type": "custom",
"name": "TimeBoundKind"
}
},
{
"doc": "",
"name": "timestamp",
"value": {
"type": "u64"
}
}
]
},
{
"type": "function",
"doc": "",
"name": "deposit",
"inputs": [
{
"doc": "",
"name": "from",
"value": {
"type": "address"
}
},
{
"doc": "",
"name": "token",
"value": {
"type": "address"
}
},
{
"doc": "",
"name": "amount",
"value": {
"type": "i128"
}
},
{
"doc": "",
"name": "claimants",
"value": {
"type": "vec",
"element": {
"type": "address"
}
}
},
{
"doc": "",
"name": "time_bound",
"value": {
"type": "custom",
"name": "TimeBound"
}
}
],
"outputs": []
}
]
from stellar_sdk import scval
secret = "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
rpc_server_url = "https://soroban-testnet.stellar.org:443"
contract_id = "CAIKIZOT2LXM2WBEPGTZTPHHTGVHGLEOSI4WE6BOHWIBHJOKHPMCOPLO"
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
kp = Keypair.from_secret(secret)
soroban_server = SorobanServer(rpc_server_url)
source = soroban_server.load_account(kp.public_key)
# Let's build a transaction that invokes the `deposit` function.
tx = (
TransactionBuilder(source, network_passphrase, base_fee=1000)
.set_timeout(300)
.append_invoke_contract_function_op(
contract_id=contract_id,
function_name="deposit",
parameters=[
scval.to_address(kp.public_key),
scval.to_address("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"),
scval.to_int128(1),
scval.to_vec(
[
scval.to_address("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"),
]
),
scval.to_map(
{
scval.to_symbol("kind"): scval.to_vec(
[
scval.to_symbol("Before"),
]
),
scval.to_symbol("timestamp"): scval.to_uint64(12346),
}
),
],
)
.build()
)
Lectura de eventos del contrato
Leer eventos del contrato es similar a leer resultados de transacciones. Puedes usar el método RPC getEvents
para obtener la lista de eventos asociados con un contrato.
Una convención común es que cadenas pequeñas como nombres de función, claves de enum y temas de eventos se representan como ScSymbol
en la especificación del contrato.
Sin embargo, los temas de eventos pueden ser de cualquier tipo scval
dependiendo de la implementación del contrato.
En el siguiente ejemplo, codificaremos el mint a ScSymbol
antes de consultarlo, y también codificaremos las direcciones a ScAddress
. Incluso después de obtener el evento, necesitaremos analizar los temas y valores para recuperar los valores reales de xdr base 64 a sus tipos correspondientes antes de convertirlos a tipos nativos.
- Rust
- Python
- bash
let address_1: &Address = "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".into();
let address_2: &Address = "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".into();
let count: i128 = 1;
env.events()
.publish((symbol_short!("mint"), address_1, address_2), count);
from stellar_sdk import SorobanServer, scval, stellar_xdr
from stellar_sdk.exceptions import NotFoundError, BadResponseError
from stellar_sdk.soroban_server import EventFilter, EventFilterType
def get_events():
server = SorobanServer("https://soroban-testnet.stellar.org")
# Define the request parameters
start_ledger = 835020
contract_id = "CDLYESZILKBHBRSPKQCQ3Q4K4N6MBI6UIQR3QXJ5L6WSYXC4EMTHSNNX"
topic = [
scval.to_symbol("mint").to_xdr(),
scval.to_address("GALIALRZJ5EU2IJJSIQEA3D3ZIEHK5HPBHZJFUEPTGQU3MYEKKIUINTY").to_xdr(),
scval.to_address("GC45QSBFYHGQUIWWQEOZ43INQGXX57CSSAABWRZ325H7MNFIFWZ56FD4").to_xdr(),
]
try:
# Use the get_events method directly
events_response = server.get_events(
start_ledger=start_ledger,
filters=[
EventFilter(
event_type=EventFilterType.CONTRACT,
contract_ids=[contract_id],
topics=[topic]
)
],
limit=20
)
# Process the response
print(f"Latest ledger: {events_response.latest_ledger}")
for event in events_response.events:
print(f"Event ID: {event.id}")
print(f"Contract ID: {event.contract_id}")
for _topic in event.topic:
if _topic is None:
continue
sc_val = stellar_xdr.SCVal.from_xdr(_topic)
if sc_val.sym is not None:
print(f"Topic: {scval.from_symbol(sc_val)}")
if sc_val.address is not None:
print(f"Topic: {scval.from_address(sc_val).address}")
if event.value is not None:
value_sc_val = stellar_xdr.SCVal.from_xdr(event.value)
if value_sc_val.i128 is not None:
print(f"Value: {scval.from_int128(value_sc_val)}")
print("---")
except NotFoundError:
print("No events found for the given parameters.")
except BadResponseError as e:
print(f"Error occurred: {str(e)}")
curl -X POST \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getEvents",
"params": {
"startLedger": 1190000,
"filters": [
{
"type": "contract",
"contractIds": [
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
],
"topics": [
[
"AAAADwAAAARtaW50",
"AAAAEgAAAAAAAAAAFoAuOU9JTSEpkiBAbHvKCHV07wnyktCPmaFNswRSkUQ=",
"AAAAEgAAAAAAAAAAudhIJcHNCiLWgR2ebQ2Br378UpAAG0c710/2NKgts98=",
]
]
}
],
"pagination": {
"limit": 20
}
}
}' \
https://soroban-testnet.stellar.org
Conclusión
Como hemos visto, trabajar con contratos inteligentes de Soroban en diferentes lenguajes de programación no es ciencia espacial, pero requiere atención cuidadosa a los detalles. Los puntos clave son:
- Siempre comienza con un sólido entendimiento de la especificación de tu contrato
- Conviértete en un experto en convertir entre tipos nativos y las peculiares estructuras de datos de Soroban
- No te intimidis por los tipos de datos complejos - son solo acertijos esperando ser resueltos
- Cuando tengas dudas, consulta la documentación de tu SDK para las sutilezas específicas del lenguaje
Guías en esta categoría:
📄️ Usar Docker para crear y ejecutar dapps
Entender Docker y usarlo para crear aplicaciones
📄️ Guía completa de frontend para dapps Stellar
Aprende a crear interfaces de frontend funcionales para dapps Stellar usando React, Tailwind CSS y el Stellar SDK.
📄️ Inicializa una dapp usando scripts
Configura la inicialización correctamente para asegurar un proceso fluido para tu dapp
📄️ Crear un frontend para tu dapp usando React
Conectar frontends de dapp a contratos y la billetera Freighter usando @soroban-react
📄️ Desarrollar plantillas de interfaz de usuario para la inicialización de contratos
Entender, encontrar y crear tus propias plantillas de interfaz de usuario para usarlas con el comando `stellar contract init` de Stellar CLI
📄️ Implementar archivo de estado en dapps
Aprender cómo implementar archivo de estado en tu dapp
📄️ Trabajar con especificaciones de contrato en Java, Python y PHP
Una guía para entender e interactuar con los contratos inteligentes de Soroban en diferentes lenguajes de programación