Saltar al contenido principal

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.

consejo

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));
}
})();

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:

  1. 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 y args son los argumentos que se pasan a esa función.

  2. 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.
  3. 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 valor contractIDPreimage 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 por address (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 cuando executable == 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.
Uso de JavaScript

Cada una de estas variaciones de invocación de funciones host tiene métodos de conveniencia en el SDK de JavaScript:

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 - contiene SorobanAddressCredentials con la siguiente estructura:
    struct SorobanAddressCredentials
    {
    SCAddress address;
    int64 nonce;
    uint32 signatureExpirationLedger;
    SCVal signature;
    };
    Los campos de esta estructura tienen la siguiente semántica:
    • 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 en signatureExpirationLedger, pero ya no es válida en signatureExpirationLedger + 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 por address hasta signatureExpirationLedger. 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 preimagen ENVELOPE_TYPE_SOROBAN_AUTHORIZATION (XDR). La estructura de la firma está definida por el contrato de cuenta correspondiente a la Address (consulta más abajo para la estructura de firma de cuenta Stellar).

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 llamada require_auth/require_auth_for_args realizada en nombre de la dirección. Ten en cuenta que si require_auth[_for_args] no fue llamado, no debe haber una entrada SorobanAuthorizedInvocation en la transacción.
  • SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN es la autorización para HOST_FUNCTION_TYPE_CREATE_CONTRACT o para la función host create_contract llamada desde un contrato. Solo contiene la estructura XDR CreateContractArgs 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 helper authorizeEntry 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 LedgerKeys 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.