Signing Soroban contract invocations
When invoking Soroban smart contracts that require authorization, there are two distinct signing approaches. Understanding when to use each method is essential for building powerful applications that interact with smart contracts.
Overviewβ
| Method | Who can Sign? | Best For |
|---|---|---|
| Transaction signing | G-accounts | G-accounts who fully trust and know how to thoroughly verify the authenticity and security of the transactions. π Note: This is the only method supported by Stellar Classic. |
| Auth-entry signing | G-accounts and C-accounts | Smart wallets, multi-party auth, stricter authorization control, relayed and/or sponsored transactions |
In both methods, the sequence number is "spent" by the transaction source account.
Method 1: Transaction signingβ
Full transaction signing is the simpler approach where the same account acts as both the transaction source (paying fees and consuming sequence) and the authorizer of the contract invocation. This method works only with G-accounts (Stellar accounts starting with G).
When to useβ
- The calling account owns the keys and can sign the full transaction
- The caller is willing to pay transaction fees, which can only be paid in XLM and by a G-account
- You want the simplest integration path
- Single-party authorization is sufficient
How it worksβ
- Client builds a transaction with an
invokeHostFunctionoperation - Client simulates the transaction to get resource requirements and footprint
- Client signs the entire transaction envelope with the source account's keypair
- Client submits the signed transaction to the network
When the transaction source account is the same as the address being authorized, the signature on the transaction itself implicitly authorizes the invocationβno separate auth entry signature is needed. This is called "source account authorization" and uses the sorobanCredentialsSourceAccount credential type.
Code exampleβ
import {
BASE_FEE,
Keypair,
nativeToScVal,
Networks,
Operation,
TransactionBuilder,
} from "@stellar/stellar-sdk";
import { Server, Api } from "@stellar/stellar-sdk/rpc";
const rpcUrl = "https://soroban-testnet.stellar.org";
const server = new Server(rpcUrl);
const sourceKeypair = Keypair.fromSecret("S...");
async function invokeWithFullSigning(
contractId: string,
recipientAddress: string,
amount: bigint,
): Promise<Api.SendTransactionResponse> {
const sourceAccount = await server.getAccount(sourceKeypair.publicKey());
const transaction = new TransactionBuilder(sourceAccount, {
fee: BASE_FEE,
networkPassphrase: Networks.TESTNET,
})
.addOperation(
Operation.invokeContractFunction({
contract: contractId,
function: "transfer",
args: [
nativeToScVal(sourceKeypair.publicKey(), { type: "address" }),
nativeToScVal(recipientAddress, { type: "address" }),
nativeToScVal(amount, { type: "i128" }),
],
}),
)
.setTimeout(30)
.build();
// prepareTransaction simulates and assembles in one step
const preparedTx = await server.prepareTransaction(transaction);
// Sign the transaction envelope
preparedTx.sign(sourceKeypair);
// Submit to network
const response = await server.sendTransaction(preparedTx);
return response;
}
For transaction signing, prepareTransaction handles simulation internally. Since the source account's signature on the transaction envelope implicitly authorizes the invocation, no separate auth entry signing is neededβand therefore no second simulation is required.
Key characteristicsβ
- Sequence number: Consumed from the source account
- Fees: Paid by the source account
- Authorization: Implicit via transaction signature (for source account credentials)
- Limitation: Cannot be used with C-accounts (contract accounts)
Method 2: Auth-entry signingβ
Auth-entry signing decouples authorization from transaction submission. The authorizer signs only the specific contract invocation (an "auth entry"), while a separate account acts as the transaction source, paying fees and consuming its own sequence number. This method works for either G-account or C-account clients.
When to useβ
- The end-user doesn't have a G-account, only a C-account (commonly a smart wallet).
- The end-user doesn't (need to) have XLM to pay for fees
- Applications want a fine-grained control over which parts of the contract invocation (and subinvocations) are authorized by each account.
- Building smart contract protocols where the end-user doesn't submit the transaction
- Building payment protocols where the transaction source account will be defined at a later point in time
How it worksβ
- Client builds the transaction using
AssembledTransaction - Client simulates (Recording Mode) to get the authorization tree
- Client signs the auth entries using
signAuthEntries - Client optionally re-simulates (Enforcing Mode) to validate their signatures
- Client sends the transaction XDR to the fee-payer
- Fee-payer parses the XDR and rebuilds with its own G-account as source
- Fee-payer performs security verifications π¨ to ensure the incoming transaction does not contain any malicious code.
- Fee-payer simulates (Enforcing Mode) to catch errors before paying fees
- Fee-payer signs the transaction envelope and submits
Simulation modes: Recording vs Enforcingβ
Transaction simulation has two modes that are critical to understand:
| Mode | When used | What it does |
|---|---|---|
| Recording Mode | First simulation, before signing | Returns the auth entries that need signatures. Skips require_auth validation. |
| Enforcing Mode | Second simulation, after signing | Validates signatures and executes __check_auth. Returns accurate resource estimates. |
The first simulation (Recording Mode) does not execute the require_auth checks β it only records which auth entries are needed. This means the resource estimates from the first simulation are incomplete.
The fee-payer must simulate in Enforcing Mode, and the client is strongly recommended to simulate as well to ensure fees and auth checks are correct when the contract enforces signatures:
| Who | Why |
|---|---|
| Client | Validates signatures before sending to fee-payer and ensures auth enforcement succeeds before it leaves the client. |
| Fee-payer | Verifies the transaction will succeed before submitting and ensures auth enforcement will pass before paying fees. |
Running Enforcing Mode simulation provides two critical benefits:
- Validates signatures and execution β Catches auth errors and contract failures before submission. Failed simulations cost nothing; failed submissions cost real fees.
- Returns accurate resource estimates β Recording Mode underestimates fees because it skips auth validation.
See Transaction Simulation - Authorization for more details.
Auth entry structureβ
An auth entry signature authorizes a specific invocation tree and includes:
- Address: The account authorizing the invocation
- Signature expiration ledger: When the signature becomes invalid (ledger-based, not timestamp)
- Nonce: A unique value for replay protection
- Signature: Signs the SHA-256 hash of the
ENVELOPE_TYPE_SOROBAN_AUTHORIZATIONpreimage
Auth entry signatures expire based on ledger numbers, not timestamps. A typical offset is between 12 and 60 ledgers (approximately 1-5 minutes). The signature is valid until and including the signatureExpirationLedger, but invalid at signatureExpirationLedger + 1.
Keep expiration windows as small as viable β shorter windows are safer and result in lower transaction costs.
Code example: Using AssembledTransactionβ
This example shows a token transfer where the sender (client) uses AssembledTransaction to build and sign auth entries, then sends the transaction XDR to a fee-payer for submission.
Step 1: Client builds and signs auth entriesβ
import { Keypair, Networks, nativeToScVal } from "@stellar/stellar-sdk";
import {
AssembledTransaction,
basicNodeSigner,
} from "@stellar/stellar-sdk/contract";
import { Api } from "@stellar/stellar-sdk/rpc";
const rpcUrl = "https://soroban-testnet.stellar.org";
const networkPassphrase = Networks.TESTNET;
// Client's keypair (authorizes the transfer)
const senderKeypair = Keypair.fromSecret("S...");
async function buildSignedAuthEntries(
tokenContractId: string,
recipientAddress: string,
amount: bigint,
): Promise<string> {
// Build transaction using AssembledTransaction
const tx = await AssembledTransaction.build({
contractId: tokenContractId,
method: "transfer",
args: [
nativeToScVal(senderKeypair.publicKey(), { type: "address" }),
nativeToScVal(recipientAddress, { type: "address" }),
nativeToScVal(amount, { type: "i128" }),
],
networkPassphrase,
rpcUrl,
parseResultXdr: (result) => result,
});
// Check simulation result (Recording Mode)
if (Api.isSimulationError(tx.simulation)) {
throw new Error(`Simulation failed: ${tx.simulation.error}`);
}
// Check who needs to sign
const missingSigners = tx.needsNonInvokerSigningBy();
if (!missingSigners.includes(senderKeypair.publicKey())) {
throw new Error("Sender not in required signers");
}
// Sign auth entries using basicNodeSigner
const signer = basicNodeSigner(senderKeypair, networkPassphrase);
await tx.signAuthEntries({
address: senderKeypair.publicKey(),
signAuthEntry: signer.signAuthEntry,
expiration: tx.simulation.latestLedger + 60, // ~5 minutes
});
// Re-simulate to validate signatures (π Enforcing Mode)
await tx.simulate();
if (Api.isSimulationError(tx.simulation)) {
throw new Error(`Signature validation failed: ${tx.simulation.error}`);
}
// Verify all signatures collected
if (tx.needsNonInvokerSigningBy().length > 0) {
throw new Error("Missing signatures");
}
// Return transaction XDR for fee-payer
return tx.built!.toXDR();
}
Step 2: Fee-payer rebuilds and submitsβ
import {
Keypair,
Networks,
Operation,
Transaction,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import { Api, assembleTransaction, Server } from "@stellar/stellar-sdk/rpc";
const rpcUrl = "https://soroban-testnet.stellar.org";
const networkPassphrase = Networks.TESTNET;
const server = new Server(rpcUrl);
const feePayerKeypair = Keypair.fromSecret("S...");
async function submitWithSignedAuth(
transactionXdr: string,
): Promise<Api.SendTransactionResponse> {
// Parse client's transaction to extract operation and Soroban data
const clientTx = new Transaction(transactionXdr, networkPassphrase);
const txEnvelope = xdr.TransactionEnvelope.fromXDR(transactionXdr, "base64");
const sorobanData = txEnvelope.v1()?.tx()?.ext()?.sorobanData();
if (!sorobanData) {
throw new Error("Missing Soroban data");
}
const invokeOp = clientTx.operations[0] as Operation.InvokeHostFunction;
// π¨ SECURITY: Verify the transaction/operation source is not the fee-payer's account, and that the auth entries do not reference the fee-payer's account
// Rebuild transaction with fee-payer as source
const feePayerAccount = await server.getAccount(feePayerKeypair.publicKey());
const rebuiltTx = new TransactionBuilder(feePayerAccount, {
fee: clientTx.fee,
networkPassphrase,
sorobanData,
})
.setTimeout(30)
.addOperation(
Operation.invokeHostFunction({
func: invokeOp.func,
auth: invokeOp.auth || [],
source: invokeOp.source,
}),
)
.build();
// π Simulate before submitting to catch errors without paying fees (Enforcing Mode)
const simResult = await server.simulateTransaction(rebuiltTx);
if (Api.isSimulationError(simResult)) {
throw new Error(`Fee-payer simulation failed: ${simResult.error}`);
}
const assembledTx = assembleTransaction(rebuiltTx, simResult).build();
// Fee-payer signs and submits
assembledTx.sign(feePayerKeypair);
return await server.sendTransaction(assembledTx);
}
Key characteristicsβ
- Sequence number: Consumed from the fee-payer account
- Fees: Paid by the fee-payer account (in XLM)
- Client authorization: Explicit via signed auth entries (separate from transaction signature)
- Flexibility: Works with both G-accounts and C-accounts (contract accounts)
- Use case: Sponsored transactions, relayed transactions, smart wallets, multi-party authorization, fine-grained authorization control
C-account (smart wallet) authorizationβ
C-accounts (contract accounts starting with C) cannot sign transaction envelopesβthey can only authorize via auth entries. This is because C-accounts don't have traditional keypairs; their authorization logic is defined by the contract code itself (e.g., a smart wallet contract that verifies passkey signatures).
For C-accounts:
- The auth entry signature is produced by the contract's authentication mechanism (e.g., passkeys, multisig logic)
- A separate G-account must always act as the transaction source to pay fees
- Wallet interfaces and signing utilities provide
signAuthEntryAPIs for this purpose
Signing auth entries with different methodsβ
Using basicNodeSigner for G-accounts (Node.js/backend)
The basicNodeSigner utility creates signing functions from a keypair, providing both signAuthEntry and signTransaction methods. This is useful for backend services or testing.
import { Keypair } from "@stellar/stellar-sdk";
import { basicNodeSigner } from "@stellar/stellar-sdk/contract";
const keypair = Keypair.fromSecret("S...");
const networkPassphrase = "Test SDF Network ; September 2015";
// Create a signer that provides both auth entry and transaction signing
const signer = basicNodeSigner(keypair, networkPassphrase);
// Sign an auth entry
const signedAuthEntry = await signer.signAuthEntry(authEntry);
// Sign a transaction envelope
const signedTransaction = await signer.signTransaction(transactionXDR);
Using Freighter for C-accounts (browser/wallet)
Wallet interfaces like Freighter provide signAuthEntry for C-accounts (smart wallets) where the signing logic is defined by the contract.
import freighterApi from "@stellar/freighter-api";
// Freighter's signAuthEntry returns the signed auth entry
const signedAuthEntry = await freighterApi.signAuthEntry(preimageXdr);
C-accounts:
- Cannot be the transaction source account
- Cannot sign transaction envelopes
- Must rely on a G-account to pay fees and submit transactions
- Require their auth entries to be signed according to their contract logic
Comparison summaryβ
| Aspect | Transaction signing | Auth-entry signing |
|---|---|---|
| Transaction source | Client (G-account) | Fee-payer (G-account) |
| Sequence consumed from | Client | Fee-payer |
| Who signs auth entries? | N/A (implicit via tx signature) | Client (G or C-account) |
| Who signs tx envelope? | Client | Fee-payer |
| Account types supported | G-accounts only | G and C-accounts |
Visual: Transaction signingβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Transaction Envelope β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β Source: Client G-account β β
β β Fees: Paid by client β β
β β βββββββββββββββββββββββββββββββββββββββββ β β
β β β invokeHostFunction Operation β β β
β β β Auth: Source account credential β β β
β β βββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β Envelope signature: Client π β
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
Visual: Auth-entry signingβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Transaction Envelope β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β Source: Fee-payer G-account β β
β β Fees: Paid by fee-payer β β
β β βββββββββββββββββββββββββββββββββββββββββ β β
β β β invokeHostFunction Operation β β β
β β β Auth: Signed auth entries βββββββββββΌβββΌβββ Client signs π
β β β (G or C-account) β β β
β β βββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β Envelope signature: Fee-payer π β
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
Real-world use case: Sponsored transactionsβ
A common pattern in payment protocols is for a fee-payer to submit transactions on behalf of users:
- Client builds a transaction and simulates it (Recording Mode)
- Client signs the auth entries (authorizing the payment)
- Client optionally re-simulates (Enforcing Mode) to validate signatures
- Client sends the signed auth entries to the fee-payer service
- Fee-payer rebuilds the transaction with its own G-account as source
- Fee-payer re-simulates (Enforcing Mode) to get accurate resource estimates and validate signatures
- Fee-payer signs the transaction envelope and submits
This allows the fee-payer to sponsor fees while the client retains exclusive control over authorizing their funds.
Note on Fee-bump transactionsβ
Regardless of the method, the user can still use fee bump transactions as an additional layer in order to separate the account spending their sequence number from the account paying the fees.
Mor info on fee bump transactions can be found in the Fee-bump transactions guide.
Common pitfalls and gotchasβ
- Using Horizon URLs: Soroban signing requires Soroban RPC, not Horizon. Refer to Soroban RPC Providers for the correct URL to use.
- Forgetting to assemble: Fee-payer flows must
assembleTransactionafter simulation to apply footprint + resource fees. - Missing auth on rebuilt ops: When rebuilding, include
sorobanDatain theinvokeHostFunctionoperation. - Wrong signer: C-accounts cannot sign envelopes, only auth entries.
- Stale auth expiration: Keep
signatureExpirationLedgershort and aligned with expected submission time.
Further readingβ
- Transaction simulation β Details on simulation and authorization modes
- Contract authorization β How
require_authworks in contracts - Sign authorization entries with Freighter β Wallet integration for auth entry signing
- Send to and receive from C-accounts β Working with contract accounts
- Fee-bump transactions β Using fee bump transactions to pay for transaction fees on behalf of another account without re-signing the transaction
Guides in this category:
ποΈ Create an account
Learn about creating Stellar accounts, keypairs, funding, and account basics.
ποΈ Send to and receive payments from Contract Accounts
Learn to send payments to and receive payments from Contract Accounts on the Stellar network.
ποΈ Send and receive payments
Learn to send payments and watch for received payments on the Stellar network.
ποΈ Channel accounts
Create channel accounts to submit transactions to the network at a high rate.
ποΈ Signing Soroban contract invocations
Learn two methods to sign Soroban smart contract invocations: full transaction signing for G-accounts and auth-entry signing for G or C-accounts with sponsored fees.
ποΈ Claimable balances
Split a payment into two parts by creating a claimable balance.
ποΈ Clawbacks
Use clawbacks to burn a specific amount of a clawback-enabled asset from a trustline or claimable balance.
ποΈ Fee-bump transactions
Use fee-bump transactions to pay for transaction fees on behalf of another account without re-signing the transaction.
ποΈ Sponsored reserves
Use sponsored reserves to pay for base reserves on behalf of another account.
ποΈ Path payments
Send a payment where the asset received differs from the asset sent.
ποΈ Pooled accounts: muxed accounts and memos
Use muxed accounts to differentiate between individual accounts in a pooled account.
ποΈ Install and deploy a smart contract with code
Install and deploy a smart contract with code.
ποΈ Invoke a contract function in a transaction using SDKs
Use the Stellar SDK to create, simulate, and assemble a transaction.
ποΈ simulateTransaction RPC method guide
simulateTransaction examples and tutorials guide.
ποΈ Submit a transaction to Stellar RPC using the JavaScript SDK
Use a looping mechanism to submit a transaction to the RPC.
ποΈ Upload WebAssembly (Wasm) bytecode using code
Upload the Wasm of the contract using js-stellar-sdk.