Saltar al contenido principal

Transacción Stellar

Ejemplo de uso del SDK

Algunos (pero no todos todavía) de los SDKs de Stellar tienen funciones integradas para gestionar la mayor parte del proceso de construir una transacción Stellar para interactuar con un contrato inteligente Soroban. A continuación, mostramos en JavaScript y Python cómo construir y enviar una transacción Stellar que invocará una instancia del contrato inteligente del ejemplo increment.

consejo

El existente JavaScript SDK ahora incorpora todos los elementos necesarios para Soroban. Solo necesitas instalarlo usando tu gestor de paquetes preferido.

npm install --save @stellar/stellar-sdk
(async () => {
const {
Keypair,
Contract,
rpc as StellarRpc,
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 StellarRpc.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 soporta invocar y desplegar contratos con una nueva operación llamada InvokeHostFunctionOp. El stellar-cli abstrae estos detalles para el usuario, pero no todos los SDKs lo hacen todavía. Si estás desarrollando una dapp, probablemente te tocará construir la transacción XDR para enviar a la red.

El InvokeHostFunctionOp puede usarse para realizar las siguientes operaciones Soroban:

  • Invocar funciones de contrato.
  • Subir Wasm de los nuevos contratos.
  • Desplegar nuevos contratos usando el Wasm subido o implementaciones incorporadas (esto actualmente incluye solo el contrato token).

Solo se permite un único InvokeHostFunctionOp por transacción. Los contratos deben usarse para realizar múltiples acciones atómicamente, por ejemplo, para desplegar un nuevo contrato e inicializarlo atómicamente.

InvokeHostFunctionOp

El XDR de HostFunction e InvokeHostFunctionOp se encuentra 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

El hostFunction en InvokeHostFunctionOp será ejecutado por el entorno host Soroban. Las funciones soportadas 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 a pasar a esa función.

  2. HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM

    • Esto subirá el Wasm del contrato usando el blob wasm proporcionado.
    • El Wasm subido puede identificarse por el hash SHA-256 del Wasm subido.
  3. HOST_FUNCTION_TYPE_CREATE_CONTRACT

    • Esto desplegará una instancia de contrato en la red usando el executable especificado. El identificador del contrato de 32 bytes se basa en el valor contractIDPreimage y el identificador de red (así cada red tiene un espacio de nombres separado para identificadores de contratos).
    struct CreateContractArgs
    {
    ContractIDPreimage contractIDPreimage;
    ContractExecutable executable;
    };
    • executable puede ser el hash SHA-256 del Wasm previamente subido o puede especificar que debe usarse 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 calculando 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 usando la dirección y la sal proporcionadas. Esta operación debe ser autorizada por address (ver la sección siguiente para más detalles).
      • CONTRACT_ID_FROM_ASSET especifica que el contrato será creado usando el activo Stellar. Esto solo se soporta cuando executable == CONTRACT_EXECUTABLE_TOKEN. Ten en cuenta que el activo no necesita existir cuando esto se aplica, sin embargo, el emisor del activo será el administrador inicial del token. Cualquiera puede desplegar contratos de activos.
Uso en JavaScript

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

Datos de autorización

El marco de autorización de Soroban provee una forma estandarizada de pasar datos de autorización a las invocaciones del contrato mediante 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 es autorizado por un usuario especificado en credentials.

SorobanAddressCredentials tiene dos opciones:

  • SOROBAN_CREDENTIALS_SOURCE_ACCOUNT - esto simplemente usa la firma de la cuenta fuente de la transacción (o la operación si existe) y por lo tanto no requiere ninguna carga 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 las siguientes semánticas:
    • Cuando address es la dirección que autoriza la invocación.
    • signatureExpirationLedger es el número de secuencia del ledger en el que la firma expira. La firma aún se considera válida en signatureExpirationLedger, pero no es válida en signatureExpirationLedger + 1. Se recomienda mantener esto lo más pequeño posible, ya que hace la transacción más económica.
    • nonce es un valor arbitrario que es único para todas las firmas realizadas por address hasta signatureExpirationLedger. Una buena práctica para generar esto es simplemente usar un valor aleatorio.
    • signature es una estructura que contiene la(s) firma(s) 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 (ver más abajo la estructura de firma de las cuentas Stellar).

SorobanAuthorizedInvocation define un nodo en el árbol de invocaciones autorizadas:

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 función de contrato o función host) y las sub-invocaciones autorizadas que function realiza (si las hay).

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 que se invoca y los argumentos de require_auth/require_auth_for_args realizados en nombre de la dirección. Ten en cuenta que si no se llamó a require_auth[_for_args], no debería haber una entrada SorobanAuthorizedInvocation en la transacción.
  • SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN es 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.

La construcción de árboles SorobanAuthorizedInvocation puede simplificarse usando el modo de grabación de autorización en el mecanismo simulateTransaction de Soroban (ver los docs para más detalles).

Firmas de cuentas Stellar

El formato signatureArgs es definido por el usuario para las custom accounts, pero está definido por el protocolo para las cuentas Stellar.

Las firmas para la cuenta Stellar son un vector de las siguientes estructuras Soroban en formato Soroban SDK:

#[contracttype]
pub struct AccountEd25519Signature {
pub public_key: BytesN<32>,
pub signature: BytesN<64>,
}
Uso en JavaScript

Hay un par de métodos útiles en el SDK para facilitar el manejo de autorizaciones:

  • Una vez que hayas obtenido las entradas de autorización de simulateTransaction, puedes usar el ayudante authorizeEntry para "completar" la entrada vacía correctamente. Por supuesto, necesitarás el firmante apropiado para cada una de las entradas si estás en una situación multiusuario.
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 deseas construir una entrada de autorización desde cero en lugar de depender de la simulación, puedes usar authorizeInvocation, que generará la estructura con los campos apropiados.

Recursos de la transacción

Cada transacción Soroban debe tener un extension SorobanTransactionData poblado. Esto es necesario para calcular la tarifa de recursos Soroban.

Los datos de la transacción Soroban se definen como sigue:

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 Soroban y refundableFee. refundableFee es la parte de la tarifa de transacción que es elegible para reembolso. Incluye las tarifas por los eventos de contrato emitidos por la transacción, el valor devuelto de la invocación de la función host, y las tarifas por el alquiler de espacio en 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 por recursos. La huella debe contener los LedgerKeys que serán leídos y/o escritos.

El método más sencillo para determinar los valores en SorobanResources y refundableFee es usar el mecanismo simulateTransaction.

Uso en JavaScript

Puedes usar SorobanDataBuilder para aprovechar el patrón Builder y obtener/configurar todos los recursos mencionados arriba adecuadamente. Luego, llamas a .build() y pasas la estructura resultante al método setSorobanData del correspondiente TransactionBuilder.