Transacción Stellar
Ejemplo de uso del SDK
Algunos (pero no todos aún) de los SDK de Stellar tienen funciones incorporadas para manejar la mayor parte del proceso de crear una transacción Stellar para interactuar con un contrato inteligente Soroban. A continuación, demostramos en JavaScript y Python cómo construir y enviar una transacción Stellar que invocará una instancia del contrato inteligente increment example.
- JavaScript
- Python
- Java
El SDK de JavaScript existente ahora incorpora todos los elementos necesarios para Soroban. Todo lo que necesitas hacer es instalarlo usando tu gestor de paquetes preferido.
npm install --save @stellar/stellar-sdk
(async () => {
const {
Keypair,
Contract,
SorobanRpc,
TransactionBuilder,
Networks,
BASE_FEE,
} = require("@stellar/stellar-sdk");
// The source account will be used to sign and send the transaction.
// GCWY3M4VRW4NXJRI7IVAU3CC7XOPN6PRBG6I5M7TAOQNKZXLT3KAH362
const sourceKeypair = Keypair.fromSecret(
"SCQN3XGRO65BHNSWLSHYIR4B65AHLDUQ7YLHGIWQ4677AZFRS77TCZRB",
);
// Configure the SDK to use the `stellar-rpc` instance of your choosing.
const server = new SorobanRpc.Server(
"https://soroban-testnet.stellar.org:443",
);
// Here we will use a deployed instance of the `increment` example contract.
const contractAddress =
"CBEOJUP5FU6KKOEZ7RMTSKZ7YLBS5D6LVATIGCESOGXSZEQ2UWQFKZW6";
const contract = new Contract(contractAddress);
// Transactions require a valid sequence number (which varies from one
// account to another). We fetch this sequence number from the RPC server.
const sourceAccount = await server.getAccount(sourceKeypair.publicKey());
// The transaction begins as pretty standard. The source account, minimum
// fee, and network passphrase are provided.
let builtTransaction = new TransactionBuilder(sourceAccount, {
fee: BASE_FEE,
networkPassphrase: Networks.TESTNET,
})
// The invocation of the `increment` function of our contract is added
// to the transaction. Note: `increment` doesn't require any parameters,
// but many contract functions do. You would need to provide those here.
.addOperation(contract.call("increment"))
// This transaction will be valid for the next 30 seconds
.setTimeout(30)
.build();
// We use the RPC server to "prepare" the transaction. This simulating the
// transaction, discovering the storage footprint, and updating the
// transaction to include that footprint. If you know the footprint ahead of
// time, you could manually use `addFootprint` and skip this step.
let preparedTransaction = await server.prepareTransaction(builtTransaction);
// Sign the transaction with the source account's keypair.
preparedTransaction.sign(sourceKeypair);
// Let's see the base64-encoded XDR of the transaction we just built.
console.log(
`Signed prepared transaction XDR: ${preparedTransaction
.toEnvelope()
.toXDR("base64")}`,
);
// Submit the transaction to the Stellar-RPC server. The RPC server will
// then submit the transaction into the network for us. Then we will have to
// wait, polling `getTransaction` until the transaction completes.
try {
let sendResponse = await server.sendTransaction(preparedTransaction);
console.log(`Sent transaction: ${JSON.stringify(sendResponse)}`);
if (sendResponse.status === "PENDING") {
let getResponse = await server.getTransaction(sendResponse.hash);
// Poll `getTransaction` until the status is not "NOT_FOUND"
while (getResponse.status === "NOT_FOUND") {
console.log("Waiting for transaction confirmation...");
// See if the transaction is complete
getResponse = await server.getTransaction(sendResponse.hash);
// Wait one second
await new Promise((resolve) => setTimeout(resolve, 1000));
}
console.log(`getTransaction response: ${JSON.stringify(getResponse)}`);
if (getResponse.status === "SUCCESS") {
// Make sure the transaction's resultMetaXDR is not empty
if (!getResponse.resultMetaXdr) {
throw "Empty resultMetaXDR in getTransaction response";
}
// Find the return value from the contract and return it
let transactionMeta = getResponse.resultMetaXdr;
let returnValue = getResponse.returnValue;
console.log(`Transaction result: ${returnValue.value()}`);
} else {
throw `Transaction failed: ${getResponse.resultXdr}`;
}
} else {
throw sendResponse.errorResultXdr;
}
} catch (err) {
// Catch and report any errors we've thrown
console.log("Sending transaction failed");
console.log(JSON.stringify(err));
}
})();
El py-stellar-base
SDK de Python tiene soporte estable para interactuar con contratos inteligentes Soroban desde v9.0.0.
pip install stellar-sdk
from typing import Optional, Union
from stellar_sdk import scval, MuxedAccount, Keypair, Network
from stellar_sdk.contract import AssembledTransaction, ContractClient
# `IncrementContractClient` is automatically generated by stellar-contract-bindings, and you do not need to write it manually.
# See https://github.com/lightsail-network/stellar-contract-bindings
# stellar-contract-bindings python --contract-id CDMARRPKAZEZMASLYONRI4LJI6X3QLDJQ647YGQANG2PDCP746BD5U73 --rpc-url https://soroban-testnet.stellar.org
class IncrementContractClient(ContractClient):
def increment(
self,
source: Union[str, MuxedAccount],
signer: Optional[Keypair] = None,
base_fee: int = 100,
transaction_timeout: int = 300,
submit_timeout: int = 30,
simulate: bool = True,
restore: bool = True,
) -> AssembledTransaction[int]:
"""Increment increments an internal counter, and returns the value."""
return self.invoke(
"increment",
[],
parse_result_xdr_fn=lambda v: scval.from_uint32(v),
source=source,
signer=signer,
base_fee=base_fee,
transaction_timeout=transaction_timeout,
submit_timeout=submit_timeout,
simulate=simulate,
restore=restore,
)
# The source account will be used to sign and send the transaction.
# GCWY3M4VRW4NXJRI7IVAU3CC7XOPN6PRBG6I5M7TAOQNKZXLT3KAH362
source_keypair = Keypair.from_secret('SCQN3XGRO65BHNSWLSHYIR4B65AHLDUQ7YLHGIWQ4677AZFRS77TCZRB')
rpc_server_url = "https://soroban-testnet.stellar.org:443"
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
contract_address = 'CDMARRPKAZEZMASLYONRI4LJI6X3QLDJQ647YGQANG2PDCP746BD5U73'
client = IncrementContractClient(contract_address, rpc_server_url, network_passphrase)
result = client.increment(source_keypair.public_key).sign_and_submit(source_keypair)
print(result)
java-stellar-sdk proporciona soporte para Soroban, visita la página del proyecto para más información.
import java.io.IOException;
import org.stellar.sdk.AccountNotFoundException;
import org.stellar.sdk.InvokeHostFunctionOperation;
import org.stellar.sdk.KeyPair;
import org.stellar.sdk.Network;
import org.stellar.sdk.PrepareTransactionException;
import org.stellar.sdk.SorobanServer;
import org.stellar.sdk.Transaction;
import org.stellar.sdk.TransactionBuilder;
import org.stellar.sdk.TransactionBuilderAccount;
import org.stellar.sdk.requests.sorobanrpc.SorobanRpcErrorResponse;
import org.stellar.sdk.responses.sorobanrpc.GetTransactionResponse;
import org.stellar.sdk.responses.sorobanrpc.SendTransactionResponse;
import org.stellar.sdk.scval.Scv;
import org.stellar.sdk.xdr.TransactionMeta;
public class SorobanExample {
public static void main(String[] args)
throws SorobanRpcErrorResponse, IOException, InterruptedException {
// The source account will be used to sign and send the transaction.
KeyPair sourceKeypair =
KeyPair.fromSecretSeed("SCQN3XGRO65BHNSWLSHYIR4B65AHLDUQ7YLHGIWQ4677AZFRS77TCZRB");
// Configure SorobanClient to use the `stellar-rpc` instance of your choosing.
SorobanServer sorobanServer = new SorobanServer("https://soroban-testnet.stellar.org");
// Here we will use a deployed instance of the `increment` example contract.
String contractAddress = "CBEOJUP5FU6KKOEZ7RMTSKZ7YLBS5D6LVATIGCESOGXSZEQ2UWQFKZW6";
// Transactions require a valid sequence number (which varies from one account to
// another). We fetch this sequence number from the RPC server.
TransactionBuilderAccount sourceAccount = null;
try {
sourceAccount = sorobanServer.getAccount(sourceKeypair.getAccountId());
} catch (AccountNotFoundException e) {
throw new RuntimeException("Account not found, please activate it first");
}
// The invocation of the `increment` function of our contract is added to the
// transaction. Note: `increment` doesn't require any parameters, but many
// contract functions do. You would need to provide those here.
InvokeHostFunctionOperation operation =
InvokeHostFunctionOperation.invokeContractFunctionOperationBuilder(
contractAddress, "increment", null)
.build();
// Create a transaction with the source account and the operation we want to invoke.
Transaction transaction =
new TransactionBuilder(sourceAccount, Network.TESTNET)
.addOperation(operation)
.setTimeout(30) // This transaction will be valid for the next 30 seconds
.setBaseFee(100) // The base fee is 100 stroops (0.00001 XLM)
.build();
// We use the RPC server to "prepare" the transaction. This simulating the
// transaction, discovering the storage footprint, and updating the transaction
// to include that footprint. If you know the footprint ahead of time, you could
// manually use `addFootprint` and skip this step.
try {
transaction = sorobanServer.prepareTransaction(transaction);
} catch (PrepareTransactionException e) {
// You should handle the error here
throw new RuntimeException(e);
}
// Sign the transaction with the source account's keypair.
transaction.sign(sourceKeypair);
// Let's see the base64-encoded XDR of the transaction we just built.
System.out.println("Signed prepared transaction XDR: " + transaction.toEnvelopeXdrBase64());
// Submit the transaction to the Stellar-RPC server. The RPC server will then
// submit the transaction into the network for us. Then we will have to wait,
// polling `getTransaction` until the transaction completes.
SendTransactionResponse response = sorobanServer.sendTransaction(transaction);
if (!SendTransactionResponse.SendTransactionStatus.PENDING.equals(response.getStatus())) {
throw new RuntimeException("Sending transaction failed");
}
// Poll `getTransaction` until the status is not "NOT_FOUND"
GetTransactionResponse getTransactionResponse;
while (true) {
System.out.println("Waiting for transaction confirmation...");
// See if the transaction is complete
getTransactionResponse = sorobanServer.getTransaction(response.getHash());
if (!GetTransactionResponse.GetTransactionStatus.NOT_FOUND.equals(
getTransactionResponse.getStatus())) {
break;
}
// Wait one second
Thread.sleep(1000);
}
System.out.println("get_transaction response: " + getTransactionResponse);
if (GetTransactionResponse.GetTransactionStatus.SUCCESS.equals(
getTransactionResponse.getStatus())) {
// Find the return value from the contract and return it
TransactionMeta transactionMeta =
TransactionMeta.fromXdrBase64(getTransactionResponse.getResultMetaXdr());
long returnValue = Scv.fromUint32(transactionMeta.getV3().getSorobanMeta().getReturnValue());
System.out.println("Transaction result: " + returnValue);
} else {
System.out.println("Transaction failed: " + getTransactionResponse.getResultXdr());
}
}
}
Uso de XDR
Stellar admite invocar y desplegar contratos con una nueva operación llamada InvokeHostFunctionOp
. El stellar-cli
abstrae estos detalles del usuario, pero no todos los SDK lo hacen aún. Si estás construyendo un dapp, probablemente te encuentres construyendo la transacción XDR para enviarla a la red.
El InvokeHostFunctionOp
puede ser utilizado para realizar las siguientes operaciones de Soroban:
- Invocar funciones de contratos.
- Subir Wasm de los nuevos contratos.
- Desplegar nuevos contratos utilizando el Wasm subido o implementaciones incorporadas (esto actualmente incluye solo el contrato de token).
Solo se permite un único InvokeHostFunctionOp
por transacción. Los contratos deben ser utilizados para realizar múltiples acciones de forma atómica, por ejemplo, desplegar un nuevo contrato e inicializarlo atómicamente.
InvokeHostFunctionOp
El XDR de HostFunction
y InvokeHostFunctionOp
a continuación se puede encontrar aquí.
union HostFunction switch (HostFunctionType type)
{
case HOST_FUNCTION_TYPE_INVOKE_CONTRACT:
InvokeContractArgs invokeContract;
case HOST_FUNCTION_TYPE_CREATE_CONTRACT:
CreateContractArgs createContract;
case HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM:
opaque wasm<>;
};
struct InvokeHostFunctionOp
{
// Host function to invoke.
HostFunction hostFunction;
// Per-address authorizations for this host function.
SorobanAuthorizationEntry auth<>;
};
Función
La hostFunction
en InvokeHostFunctionOp
será ejecutada por el entorno host de Soroban. Las funciones admitidas son:
-
HOST_FUNCTION_TYPE_INVOKE_CONTRACT
- Esto invocará una función del contrato desplegado con los argumentos especificados en la estructura
invokeContract
.
struct InvokeContractArgs {
SCAddress contractAddress;
SCSymbol functionName;
SCVal args<>;
};contractAddress
es la dirección del contrato a invocar,functionName
es el nombre de la función a invocar yargs
son los argumentos que se pasan a esa función. - Esto invocará una función del contrato desplegado con los argumentos especificados en la estructura
-
HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM
- Esto subirá el Wasm del contrato utilizando el blob
wasm
proporcionado. - El Wasm subido puede ser identificado por el hash SHA-256 del Wasm subido.
- Esto subirá el Wasm del contrato utilizando el blob
-
HOST_FUNCTION_TYPE_CREATE_CONTRACT
- Esto desplegará una instancia del contrato en la red utilizando el
executable
especificado. El identificador de contrato de 32 bytes se basa en el valorcontractIDPreimage
y el identificador de la red (por lo que cada red tiene un espacio de nombres de identificadores de contrato separado).
struct CreateContractArgs
{
ContractIDPreimage contractIDPreimage;
ContractExecutable executable;
};executable
puede ser el hash SHA-256 del Wasm subido previamente o puede especificar que se debe usar un contrato incorporado:
enum ContractExecutableType
{
CONTRACT_EXECUTABLE_WASM = 0,
CONTRACT_EXECUTABLE_TOKEN = 1
};
union ContractExecutable switch (ContractExecutableType type)
{
case CONTRACT_EXECUTABLE_WASM:
Hash wasm_hash;
case CONTRACT_EXECUTABLE_TOKEN:
void;
};-
contractIDPreimage
se define de la siguiente manera:union ContractIDPreimage switch (ContractIDPreimageType type)
{
case CONTRACT_ID_PREIMAGE_FROM_ADDRESS:
struct
{
SCAddress address;
uint256 salt;
} fromAddress;
case CONTRACT_ID_PREIMAGE_FROM_ASSET:
Asset fromAsset;
};- El identificador final del contrato se crea computando el SHA-256 de esto junto con el identificador de red como parte de
HashIDPreimage
:
union HashIDPreimage switch (EnvelopeType type)
{
...
case ENVELOPE_TYPE_CONTRACT_ID:
struct
{
Hash networkID;
ContractIDPreimage contractIDPreimage;
} contractID;
...CONTRACT_ID_PREIMAGE_FROM_ADDRESS
especifica que el contrato será creado utilizando la dirección y la sal provista. Esta operación tiene que ser autorizada poraddress
(consulta la sección siguiente para más detalles).CONTRACT_ID_FROM_ASSET
especifica que el contrato se creará utilizando el activo Stellar. Esto solo es compatible cuandoexecutable == CONTRACT_EXECUTABLE_TOKEN
. Ten en cuenta que el activo no necesita existir cuando se aplica esto, sin embargo, el emisor del activo será el administrador inicial del token. Cualquiera puede desplegar contratos de activos.
- El identificador final del contrato se crea computando el SHA-256 de esto junto con el identificador de red como parte de
- Esto desplegará una instancia del contrato en la red utilizando el
Uso de JavaScript
Cada una de estas variaciones de invocación de funciones host tiene métodos de conveniencia en el SDK de JavaScript:
Operation.invokeHostFunction
es el método de más bajo nivel que corresponde directamente al XDR.Operation.invokeContractFunction
es una abstracción para invocar el método de un contrato en particular, similar aContract.call
.Operation.createStellarAssetContract
yOperation.createCustomContract
son abstracciones para instanciar contratos: el primero es para envolver un activo Stellar existente en un contrato inteligente y el segundo es para desplegar tu propio contrato.Operation.uploadContractWasm
corresponde a la varianteHOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM
anterior, permitiéndote subir el buffer WASM crudo al ledger.
Datos de autorización
El marco de autorización de Soroban proporciona una forma estandarizada de pasar datos de autorización a las invocaciones de contrato a través de estructuras SorobanAuthorizationEntry
.
struct SorobanAuthorizationEntry
{
SorobanCredentials credentials;
SorobanAuthorizedInvocation rootInvocation;
};
union SorobanCredentials switch (SorobanCredentialsType type)
{
case SOROBAN_CREDENTIALS_SOURCE_ACCOUNT:
void;
case SOROBAN_CREDENTIALS_ADDRESS:
SorobanAddressCredentials address;
};
SorobanAuthorizationEntry
contiene un árbol de invocaciones con rootInvocation
como raíz. Este árbol está autorizado por un usuario especificado en credentials
.
SorobanAddressCredentials
tiene dos opciones:
SOROBAN_CREDENTIALS_SOURCE_ACCOUNT
- esto simplemente utiliza la firma de la cuenta de origen de la transacción (o de la operación, si hay alguna) y por lo tanto no requiere ninguna carga útil adicional.SOROBAN_CREDENTIALS_ADDRESS
- contieneSorobanAddressCredentials
con la siguiente estructura:Los campos de esta estructura tienen la siguiente semántica:struct SorobanAddressCredentials
{
SCAddress address;
int64 nonce;
uint32 signatureExpirationLedger;
SCVal signature;
};- Cuando
address
es la dirección que autoriza la invocación. signatureExpirationLedger
el número de secuencia del ledger en el cual la firma caduca. La firma sigue siendo considerada válida ensignatureExpirationLedger
, pero ya no es válida ensignatureExpirationLedger + 1
. Se recomienda mantener esto lo más pequeño posible, ya que hace que la transacción sea más barata.nonce
es un valor arbitrario que es único para todas las firmas realizadas poraddress
hastasignatureExpirationLedger
. Un buen enfoque para generar esto es simplemente usar un valor aleatorio.signature
es una estructura que contiene la firma (o múltiples firmas) que firmaron el hash SHA-256 de 32 bytes del preimagenENVELOPE_TYPE_SOROBAN_AUTHORIZATION
(XDR). La estructura de la firma está definida por el contrato de cuenta correspondiente a laAddress
(consulta más abajo para la estructura de firma de cuenta Stellar).
- Cuando
SorobanAuthorizedInvocation
define un nodo en el árbol de invocación autorizada:
struct SorobanAuthorizedInvocation
{
SorobanAuthorizedFunction function;
SorobanAuthorizedInvocation subInvocations<>;
};
union SorobanAuthorizedFunction switch (SorobanAuthorizedFunctionType type)
{
case SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN:
SorobanAuthorizedContractFunction contractFn;
case SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN:
CreateContractArgs createContractHostFn;
};
struct SorobanAuthorizedContractFunction
{
SCAddress contractAddress;
SCSymbol functionName;
SCVec args;
};
SorobanAuthorizedInvocation
consiste en la function
que está siendo autorizada (ya sea contrato o función host) y las sub-invocaciones autorizadas que function
realiza (si hay alguna).
SorobanAuthorizedFunction
tiene dos variantes:
SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN
es una función de contrato que incluye la dirección del contrato, el nombre de la función invocada y los argumentos de la llamadarequire_auth
/require_auth_for_args
realizada en nombre de la dirección. Ten en cuenta que sirequire_auth[_for_args]
no fue llamado, no debe haber una entradaSorobanAuthorizedInvocation
en la transacción.SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN
es la autorización paraHOST_FUNCTION_TYPE_CREATE_CONTRACT
o para la función hostcreate_contract
llamada desde un contrato. Solo contiene la estructura XDRCreateContractArgs
correspondiente al contrato creado.
Construir árboles de SorobanAuthorizedInvocation
puede simplificarse utilizando el modo de autocompletado en el mecanismo simulateTransaction
de Soroban (consulta los docs para más detalles).
Firmas de cuenta Stellar
El formato de signatureArgs
es definido por el usuario para las cuentas personalizadas, pero es definido por el protocolo para las cuentas Stellar.
Las firmas para la cuenta Stellar son un vector de las siguientes estructuras de Soroban en el formato del SDK de Soroban:
#[contracttype]
pub struct AccountEd25519Signature {
pub public_key: BytesN<32>,
pub signature: BytesN<64>,
}
Uso de JavaScript
Hay un par de métodos útiles en el SDK para facilitar el manejo de autorización:
- Una vez que hayas obtenido las entradas de autorización de
simulateTransaction
, puedes usar el helperauthorizeEntry
para "completar" la entrada vacía en consecuencia. Por supuesto, necesitarás el firmante apropiado para cada una de las entradas si estás en una situación de múltiples partes.
const signedEntries = simTx.auth.map(async (entry) =>
// In this case, you can authorize by signing the transaction with the
// corresponding source account.
entry.switch() ===
xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount()
? entry
: await authorizeEntry(
entry,
// The `signer` here will be unique for each entry, perhaps reaching out
// to a separate entity.
signer,
currentLedger + 1000,
Networks.TESTNET,
),
);
- Si, en cambio, quieres construir una entrada de autorización desde cero en lugar de confiar en la simulación, puedes usar
authorizeInvocation
, que construirá la estructura con los campos apropiados.
Recursos de transacción
Cada transacción Soroban debe tener una extensión de transacción SorobanTransactionData
poblada. Esto es necesario para calcular la tarifa de recursos de Soroban.
Los datos de transacción Soroban se definen de la siguiente manera:
struct SorobanResources
{
// The ledger footprint of the transaction.
LedgerFootprint footprint;
// The maximum number of instructions this transaction can use
uint32 instructions;
// The maximum number of bytes this transaction can read from ledger
uint32 readBytes;
// The maximum number of bytes this transaction can write to ledger
uint32 writeBytes;
};
struct SorobanTransactionData
{
SorobanResources resources;
// Portion of transaction `fee` allocated to refundable fees.
int64 refundableFee;
ExtensionPoint ext;
};
Estos datos comprenden dos partes: recursos de Soroban y la refundableFee
. La refundableFee
es la parte de la tarifa de transacción elegible para reembolso. Incluye las tarifas por los eventos de contrato emitidos por la transacción, el valor de retorno de la invocación de la función host y las tarifas por el alquiler del espacio de ledger.
La estructura SorobanResources
incluye la huella del ledger y los valores de recursos, que juntos determinan el límite de consumo de recursos y la tarifa de recursos. La huella debe contener las LedgerKey
s que serán leídas y/o escritas.
El método más simple para determinar los valores en SorobanResources
y refundableFee
es usar el mecanismo simulateTransaction
.
Uso de JavaScript
Puedes usar el SorobanDataBuilder
para aprovechar el patrón de constructor y obtener/establish todos los recursos anteriores en consecuencia. Luego, llamas a .build()
y pasas la estructura resultante al método setSorobanData
del correspondiente TransactionBuilder
.