This document provides comprehensive API documentation for all Gathera smart contracts, including function signatures, parameters, return values, usage examples, and gas costs.
- Overview
- Ticket Contract API
- Escrow Contract API
- Multi-Signature Wallet Contract API
- Integration Layer API
- Decode Error Taxonomy and Retry Mapping
- Soroban Trace Replay API
- Soroban Verification API
- Error Handling
- Gas Cost Analysis
- Usage Examples
- Soroban Trace Store Schema
- Blockchain Event Task Queue
The Gathera platform consists of three main smart contracts:
- SoulboundTicketContract: Manages non-transferable event tickets
- EscrowContract: Handles secure fund escrow with dispute resolution
- MultisigWalletContract: Provides multi-signature wallet functionality
All contracts are built on the Stellar Soroban platform using Rust and compile to WebAssembly.
[TBD] (To be populated after deployment)
Issues a new soulbound ticket to a recipient.
pub fn issue_ticket(
env: Env,
event_id: Symbol,
recipient: Address,
metadata: String,
) -> Result<Symbol, TicketError>Parameters:
env(Env): The contract environmentevent_id(Symbol): Unique identifier for the eventrecipient(Address): Stellar address of the ticket recipientmetadata(String): Additional ticket metadata (JSON format)
Returns:
Result<Symbol, TicketError>:Ok(Symbol): Unique ticket identifierErr(TicketError): Error if ticket issuance fails
Gas Cost: ~15,000 gas units
Example:
use soroban_sdk::{Symbol, Address, String, Env};
let env = Env::default();
let event_id = Symbol::new(&env, "conf2024");
let recipient = Address::from_string(&env, "GD...");
let metadata = String::from_str(&env, r#"{"type": "VIP", "seat": "A1"}"#);
let ticket_id = SoulboundTicketContract::issue_ticket(
env,
event_id,
recipient,
metadata
)?;Verifies if a given address owns a specific ticket.
pub fn verify_ownership(
env: Env,
ticket_id: Symbol,
claimed_owner: Address,
) -> boolParameters:
env(Env): The contract environmentticket_id(Symbol): Unique identifier for the ticketclaimed_owner(Address): Address claiming ownership
Returns:
bool: True if the claimed_owner owns the ticket, false otherwise
Gas Cost: ~5,000 gas units
Example:
let is_owner = SoulboundTicketContract::verify_ownership(
env,
ticket_id,
claimed_address
);Retrieves ticket information.
pub fn get_ticket(env: Env, ticket_id: Symbol) -> Result<Ticket, TicketError>Parameters:
env(Env): The contract environmentticket_id(Symbol): Unique identifier for the ticket
Returns:
Result<Ticket, TicketError>:Ok(Ticket): Ticket data structureErr(TicketError): Error if ticket not found
Gas Cost: ~8,000 gas units
Example:
let ticket = SoulboundTicketContract::get_ticket(env, ticket_id)?;
println!("Ticket owner: {}", ticket.owner);
println!("Issued at: {}", ticket.issued_at);pub struct Ticket {
pub ticket_id: Symbol,
pub event_id: Symbol,
pub owner: Address,
pub issued_at: u64,
pub metadata: String,
}pub enum TicketError {
TicketAlreadyExists = 1,
TicketNotFound = 2,
Unauthorized = 3,
InvalidEventId = 4,
NotTransferable = 5,
EventEnded = 6,
MaxTicketsReached = 7,
NotImplemented = 255,
}[TBD] (To be populated after deployment)
Creates a new escrow arrangement.
pub fn create_escrow(
env: Env,
beneficiary: Address,
amount: u128,
expires_at: u64,
terms: String,
required_confirmations: u32,
) -> Result<Symbol, EscrowError>Parameters:
env(Env): The contract environmentbeneficiary(Address): Address of the beneficiaryamount(u128): Amount to escrow (in stroops)expires_at(u64): Expiration timestamp (Unix timestamp)terms(String): Escrow terms and conditionsrequired_confirmations(u32): Number of confirmations needed for release
Returns:
Result<Symbol, EscrowError>:Ok(Symbol): Unique escrow identifierErr(EscrowError): Error if escrow creation fails
Gas Cost: ~25,000 gas units
Example:
let beneficiary = Address::from_string(&env, "GD...");
let amount = 100_000_0000; // 100 XLM in stroops
let expires_at = 1704067200; // January 1, 2024
let terms = String::from_str(&env, "Payment for event ticket");
let required_confirmations = 2;
let escrow_id = EscrowContract::create_escrow(
env,
beneficiary,
amount,
expires_at,
terms,
required_confirmations
)?;Funds an existing escrow with tokens.
pub fn fund_escrow(env: Env, escrow_id: Symbol) -> Result<bool, EscrowError>Parameters:
env(Env): The contract environmentescrow_id(Symbol): Unique identifier for the escrow
Returns:
Result<bool, EscrowError>:Ok(bool): True if funding was successfulErr(EscrowError): Error if funding fails
Gas Cost: ~10,000 gas units + token transfer cost
Example:
let success = EscrowContract::fund_escrow(env, escrow_id)?;Releases funds from escrow to the beneficiary.
pub fn release_funds(env: Env, escrow_id: Symbol) -> Result<bool, EscrowError>Parameters:
env(Env): The contract environmentescrow_id(Symbol): Unique identifier for the escrow
Returns:
Result<bool, EscrowError>:Ok(bool): True if release was successfulErr(EscrowError): Error if release fails
Gas Cost: ~15,000 gas units + token transfer cost
Example:
let success = EscrowContract::release_funds(env, escrow_id)?;Creates a dispute for an escrow.
pub fn create_dispute(
env: Env,
escrow_id: Symbol,
reason: String,
) -> Result<Symbol, EscrowError>Parameters:
env(Env): The contract environmentescrow_id(Symbol): Unique identifier for the escrowreason(String): Dispute reason description
Returns:
Result<Symbol, EscrowError>:Ok(Symbol): Unique dispute identifierErr(EscrowError): Error if dispute creation fails
Gas Cost: ~12,000 gas units
Example:
let reason = String::from_str(&env, "Service not delivered as agreed");
let dispute_id = EscrowContract::create_dispute(env, escrow_id, reason)?;Resolves a dispute with specified resolution.
pub fn resolve_dispute(
env: Env,
dispute_id: Symbol,
resolution: String,
) -> Result<bool, EscrowError>Parameters:
env(Env): The contract environmentdispute_id(Symbol): Unique identifier for the disputeresolution(String): Dispute resolution details
Returns:
Result<bool, EscrowError>:Ok(bool): True if resolution was successfulErr(EscrowError): Error if resolution fails
Gas Cost: ~18,000 gas units
Example:
let resolution = String::from_str(&env, "Partial refund to beneficiary");
let success = EscrowContract::resolve_dispute(env, dispute_id, resolution)?;Retrieves escrow information.
pub fn get_escrow(env: Env, escrow_id: Symbol) -> Result<Escrow, EscrowError>Parameters:
env(Env): The contract environmentescrow_id(Symbol): Unique identifier for the escrow
Returns:
Result<Escrow, EscrowError>:Ok(Escrow): Escrow data structureErr(EscrowError): Error if escrow not found
Gas Cost: ~8,000 gas units
Example:
let escrow = EscrowContract::get_escrow(env, escrow_id)?;
println!("Escrow amount: {}", escrow.amount);
println!("Status: {:?}", escrow.status);pub struct Escrow {
pub escrow_id: Symbol,
pub depositor: Address,
pub beneficiary: Address,
pub amount: u128,
pub status: EscrowStatus,
pub created_at: u64,
pub expires_at: u64,
pub terms: String,
pub required_confirmations: u32,
pub confirmations: Vec<Address>,
}pub enum EscrowStatus {
Pending = 0,
Funded = 1,
Completed = 2,
Disputed = 3,
Refunded = 4,
Expired = 5,
}pub struct Dispute {
pub dispute_id: Symbol,
pub escrow_id: Symbol,
pub initiator: Address,
pub reason: String,
pub resolved: bool,
pub resolution: Option<String>,
}pub enum EscrowError {
EscrowAlreadyExists = 1,
EscrowNotFound = 2,
Unauthorized = 3,
InsufficientFunds = 4,
InvalidTerms = 5,
AlreadyCompleted = 6,
DisputeExists = 7,
InvalidResolution = 8,
EscrowExpired = 9,
NotImplemented = 255,
}[TBD] (To be populated after deployment)
Initializes the multi-signature wallet with owners and settings.
pub fn initialize(
env: Env,
owners: Vec<Address>,
threshold: u32,
timelock: u64,
max_amount: u128,
) -> Result<bool, MultisigError>Parameters:
env(Env): The contract environmentowners(Vec): List of initial wallet ownersthreshold(u32): Number of signatures required (1 ≤ threshold ≤ owners.len())timelock(u64): Time-lock period in secondsmax_amount(u128): Maximum transaction amount
Returns:
Result<bool, MultisigError>:Ok(bool): True if initialization was successfulErr(MultisigError): Error if initialization fails
Gas Cost: ~30,000 gas units
Example:
let mut owners = Vec::new(&env);
owners.push_back(Address::from_string(&env, "GD..."));
owners.push_back(Address::from_string(&env, "GD..."));
owners.push_back(Address::from_string(&env, "GD..."));
let threshold = 2;
let timelock = 3600; // 1 hour
let max_amount = 1_000_000_000; // 1000 XLM
let success = MultisigWalletContract::initialize(
env,
owners,
threshold,
timelock,
max_amount
)?;Submits a new transaction for approval.
pub fn submit_transaction(
env: Env,
destination: Address,
amount: u128,
data: String,
expires_at: u64,
) -> Result<Symbol, MultisigError>Parameters:
env(Env): The contract environmentdestination(Address): Recipient addressamount(u128): Amount to transfer (in stroops)data(String): Transaction data/payloadexpires_at(u64): Expiration timestamp
Returns:
Result<Symbol, MultisigError>:Ok(Symbol): Unique transaction identifierErr(MultisigError): Error if submission fails
Gas Cost: ~20,000 gas units
Example:
let destination = Address::from_string(&env, "GD...");
let amount = 50_000_000; // 5 XLM
let data = String::from_str(&env, "Payment for event services");
let expires_at = 1704067200 + 86400; // 24 hours from now
let tx_id = MultisigWalletContract::submit_transaction(
env,
destination,
amount,
data,
expires_at
)?;Approves a pending transaction.
pub fn approve_transaction(
env: Env,
transaction_id: Symbol,
) -> Result<bool, MultisigError>Parameters:
env(Env): The contract environmenttransaction_id(Symbol): Unique identifier for the transaction
Returns:
Result<bool, MultisigError>:Ok(bool): True if approval was successfulErr(MultisigError): Error if approval fails
Gas Cost: ~10,000 gas units
Example:
let success = MultisigWalletContract::approve_transaction(env, tx_id)?;Executes an approved transaction.
pub fn execute_transaction(
env: Env,
transaction_id: Symbol,
) -> Result<bool, MultisigError>Parameters:
env(Env): The contract environmenttransaction_id(Symbol): Unique identifier for the transaction
Returns:
Result<bool, MultisigError>:Ok(bool): True if execution was successfulErr(MultisigError): Error if execution fails
Gas Cost: ~15,000 gas units + token transfer cost
Example:
let success = MultisigWalletContract::execute_transaction(env, tx_id)?;Adds a new owner to the wallet (requires governing transaction).
pub fn add_owner(
env: Env,
new_owner: Address,
transaction_id: Symbol,
) -> Result<bool, MultisigError>Parameters:
env(Env): The contract environmentnew_owner(Address): Address of the new ownertransaction_id(Symbol): Governing transaction ID
Returns:
Result<bool, MultisigError>:Ok(bool): True if owner addition was successfulErr(MultisigError): Error if addition fails
Gas Cost: ~12,000 gas units
Example:
let new_owner = Address::from_string(&env, "GD...");
let success = MultisigWalletContract::add_owner(env, new_owner, gov_tx_id)?;Removes an owner from the wallet (requires governing transaction).
pub fn remove_owner(
env: Env,
owner_to_remove: Address,
transaction_id: Symbol,
) -> Result<bool, MultisigError>Parameters:
env(Env): The contract environmentowner_to_remove(Address): Address of the owner to removetransaction_id(Symbol): Governing transaction ID
Returns:
Result<bool, MultisigError>:Ok(bool): True if owner removal was successfulErr(MultisigError): Error if removal fails
Gas Cost: ~12,000 gas units
Example:
let owner_to_remove = Address::from_string(&env, "GD...");
let success = MultisigWalletContract::remove_owner(env, owner_to_remove, gov_tx_id)?;Changes the signature threshold (requires governing transaction).
pub fn change_threshold(
env: Env,
new_threshold: u32,
transaction_id: Symbol,
) -> Result<bool, MultisigError>Parameters:
env(Env): The contract environmentnew_threshold(u32): New threshold valuetransaction_id(Symbol): Governing transaction ID
Returns:
Result<bool, MultisigError>:Ok(bool): True if threshold change was successfulErr(MultisigError): Error if change fails
Gas Cost: ~10,000 gas units
Example:
let new_threshold = 3;
let success = MultisigWalletContract::change_threshold(env, new_threshold, gov_tx_id)?;Retrieves transaction information.
pub fn get_transaction(
env: Env,
transaction_id: Symbol,
) -> Result<Transaction, MultisigError>Parameters:
env(Env): The contract environmenttransaction_id(Symbol): Unique identifier for the transaction
Returns:
Result<Transaction, MultisigError>:Ok(Transaction): Transaction data structureErr(MultisigError): Error if transaction not found
Gas Cost: ~8,000 gas units
Example:
let transaction = MultisigWalletContract::get_transaction(env, tx_id)?;
println!("Amount: {}", transaction.amount);
println!("Status: {:?}", transaction.status);Retrieves current wallet configuration.
pub fn get_config(env: Env) -> MultisigConfigParameters:
env(Env): The contract environment
Returns:
MultisigConfig: Current wallet configuration
Gas Cost: ~5,000 gas units
Example:
let config = MultisigWalletContract::get_config(env);
println!("Threshold: {}", config.threshold);
println!("Owners: {:?}", config.owners);pub struct Transaction {
pub transaction_id: Symbol,
pub destination: Address,
pub amount: u128,
pub data: String,
pub status: TransactionStatus,
pub created_at: u64,
pub expires_at: u64,
pub required_confirmations: u32,
pub confirmations: Vec<Address>,
pub creator: Address,
}pub enum TransactionStatus {
Pending = 0,
Approved = 1,
Rejected = 2,
Executed = 3,
Expired = 4,
}pub struct MultisigConfig {
pub owners: Vec<Address>,
pub threshold: u32,
pub timelock: u64,
pub max_transaction_amount: u128,
}pub enum MultisigError {
TransactionAlreadyExists = 1,
TransactionNotFound = 2,
Unauthorized = 3,
InsufficientSignatures = 4,
InvalidOwner = 5,
ThresholdNotMet = 6,
AlreadyExecuted = 7,
InvalidTransaction = 8,
WalletLocked = 9,
DuplicateSignature = 10,
NotImplemented = 255,
}Creates a complete event ticketing setup with escrow integration.
pub fn create_event_with_escrow(
&self,
event_id: Symbol,
organizer: Address,
ticket_price: u128,
max_tickets: u32,
escrow_terms: String,
) -> Result<Symbol, WorkflowError>Parameters:
event_id(Symbol): Unique identifier for the eventorganizer(Address): Event organizer addressticket_price(u128): Price per ticketmax_tickets(u32): Maximum number of ticketsescrow_terms(String): Escrow terms and conditions
Returns:
Result<Symbol, WorkflowError>:Ok(Symbol): Workflow transaction identifierErr(WorkflowError): Error if workflow fails
Gas Cost: ~50,000 gas units
Processes a ticket purchase with escrow integration.
pub fn process_ticket_purchase(
&self,
event_id: Symbol,
buyer: Address,
payment_amount: u128,
) -> Result<Symbol, WorkflowError>Parameters:
event_id(Symbol): Unique identifier for the eventbuyer(Address): Buyer addresspayment_amount(u128): Payment amount
Returns:
Result<Symbol, WorkflowError>:Ok(Symbol): Transaction identifierErr(WorkflowError): Error if purchase fails
Gas Cost: ~35,000 gas units
Retrieves all contract addresses.
pub fn get_addresses(&self) -> (Address, Address, Address)Returns:
(Address, Address, Address): Tuple of (ticket, escrow, multisig) addresses
Gas Cost: ~3,000 gas units
Operators reading the Dead Letter Queue for the blockchain-events worker see
two distinct failure classes for Soroban event decode:
| Outcome code | Permanent (DLQ on first failure) | Meaning |
|---|---|---|
InvalidBase64 |
✅ | The input is not valid base64. |
TruncatedXdr |
✅ | The XDR ends before the event is complete. |
MalformedXdr |
✅ | The XDR is not a structurally valid DiagnosticEvent. |
UnsupportedEvent |
✅ | Valid XDR, but not an LMS contract event. |
UnexpectedContractId |
✅ | The emitter does not match the expected deployment. |
InvalidPayload |
✅ | Event fields do not match the documented LMS representation. |
A permanent decode failure is routed to the Dead Letter Queue on the first
attempt (thrown as UnrecoverableError) — it never retries, so a single
poisoned event cannot occupy a bounded worker concurrency slot across pointless
retries or stall unrelated jobs behind it.
Transient failures (for example a not-yet-confirmed transaction, an RPC
hiccup, or any plain Error) are retried through the ordinary attempts
machinery and are moved to the DLQ only after opts.attempts are exhausted.
The existing DLQ contract — intermediate failures are never moved, exhausted
jobs move exactly once, and job identity and error message are preserved —
applies unchanged on both axes.
Decode error messages never embed the offending input.
Operational surface for replaying/backfilling Soroban event traces into the durable trace store without deploying new code. It is idempotent and crash-safe: a durable per-network cursor records the last fully-ingested ledger, and any interrupted range is simply replayed on the next run.
Triggers a bounded backfill over a ledger range.
Request body (all optional):
| Field | Type | Description |
|---|---|---|
networkId |
string | Network key, defaults to stellar. |
fromSeq |
number | First ledger to walk. Omitted → resume from the durable cursor. |
toSeq |
number | Upper bound (defaults to the network head at enqueue time). |
batchSize |
number | Ledgers per cursor commit; smaller values make aborts cheaper. |
Response (HTTP 202 Accepted): { success, jobId, queueName: "soroban:replay", fromSeq, toSeq, timestamp }.
Guarantees:
- Idempotent — re-running a covered range inserts no duplicate traces.
- Crash-safe — a hang/crash mid-range leaves the cursor at the last fully
ingested ledger; the next run resumes from
cursor + 1with no gaps or duplicates. - Overlap-safe — concurrent live extraction and backfill absorb one another; the union is stored exactly once.
soroban:replayBullMQ queue — carries replay job payloads{ networkId, fromSeq, toSeq, batchSize }.SorobanReplayService.runBackfill— walks[fromSeq, toSeq], fetches events per ledger, ingests each trace through theSorobanTraceIngestPort, and advances the cursor atomically per batch.ReplayCursorService—getCursor,resumeFrom,advanceWithBatch(transactional upsert; never regresses the cursor).StellarProvider.getLatestLedger/eventsForLedger— Soroban RPC head query and per-ledger event extraction.
On-demand verification of a Soroban transaction issued through the
blockchain-events queue (action: "verify", networkId: "stellar"). It
fetches the transaction via Soroban RPC getTransaction, categorizes its
confirmation state, and proves the expected event is still present with equal
business fields. Every confirmed outcome is persisted durably by transaction
hash, so it survives a restart and re-verification updates the same record.
| Outcome | Meaning |
|---|---|
verified |
Transaction confirmed (SUCCESS) and it contains the expected event with equal business fields. |
mismatch |
Confirmed SUCCESS, but the expected event is absent or has different business fields. |
reverted |
Confirmed but the call failed (FAILED), e.g. resultCode=txv_bad_auth. |
| (retryable) | Transaction not found / not yet confirmed, or an RPC transport error. Retried through the ordinary attempts machinery; moved to the Dead Letter Queue only after attempts are exhausted. |
A job whose parameters.transactionHash is missing or malformed is rejected as
an unrecoverable failure (no retry, DLQ on the first attempt).
action: "verify" job against a stellar worker:
| Field | Required | Description |
|---|---|---|
parameters.transactionHash |
yes | The 0x-prefixed Soroban transaction hash to verify. |
parameters.expectedPayload |
no | Business fields the decoded event must carry to count as equal. |
eventName |
yes | The event name the transaction is expected to have emitted. |
{
"outcome": "verified",
"verified": true,
"transactionHash": "0x…",
"ledgerSeq": 42,
"eventName": "course_created"
}soroban_verifications (migration 003-create-soroban-verifications) — one
row per transactionHash, holding the outcome, the verified flag, and the
ledger sequence at which the fact was established. Re-running verification of
an already-verified transaction updates the row and never duplicates it. The
verify action remains fully supported on EVM networks (receipt/log matching).
All contracts use comprehensive error handling with specific error codes:
- Authorization Errors: Verify caller permissions
- Validation Errors: Check input parameters
- State Errors: Verify contract state
- Business Logic Errors: Enforce business rules
- Check error codes for specific failure reasons
- Use appropriate retry strategies for temporary failures
- Implement proper error logging and monitoring
| Function | Min Cost | Max Cost | Notes |
|---|---|---|---|
issue_ticket |
15,000 | 25,000 | Depends on metadata size |
verify_ownership |
5,000 | 8,000 | Simple lookup |
get_ticket |
8,000 | 12,000 | Data retrieval |
create_escrow |
25,000 | 40,000 | Depends on terms length |
fund_escrow |
10,000 | 15,000 | + token transfer |
release_funds |
15,000 | 25,000 | + token transfer |
create_dispute |
12,000 | 18,000 | Depends on reason length |
resolve_dispute |
18,000 | 30,000 | Complex logic |
initialize |
30,000 | 45,000 | Depends on owner count |
submit_transaction |
20,000 | 30,000 | Depends on data size |
approve_transaction |
10,000 | 15,000 | Simple approval |
execute_transaction |
15,000 | 25,000 | + token transfer |
- Batch Operations: Combine multiple operations when possible
- Metadata Optimization: Keep metadata strings concise
- Storage Patterns: Optimize data storage layouts
- Event Logging: Use events instead of storage when possible
use soroban_sdk::{Symbol, Address, String, Env, Vec};
fn setup_complete_event(env: Env) -> Result<(), Box<dyn std::error::Error>> {
// 1. Initialize multisig wallet
let mut owners = Vec::new(&env);
owners.push_back(Address::from_string(&env, "GD...organizer1"));
owners.push_back(Address::from_string(&env, "GD...organizer2"));
MultisigWalletContract::initialize(
env.clone(),
owners,
2, // threshold
3600, // 1 hour timelock
1_000_000_000 // 1000 XLM max
)?;
// 2. Create event with escrow
let workflow = EventTicketingWorkflow::new(env.clone());
let event_id = Symbol::new(&env, "tech_conf_2024");
let organizer = Address::from_string(&env, "GD...organizer");
let ticket_price = 100_000_000; // 10 XLM
let max_tickets = 500;
let escrow_terms = String::from_str(&env, "Refundable until event start");
workflow.create_event_with_escrow(
event_id,
organizer,
ticket_price,
max_tickets,
escrow_terms
)?;
// 3. Issue tickets to initial participants
let participants = vec![
"GD...user1",
"GD...user2",
"GD...user3"
];
for (i, participant) in participants.iter().enumerate() {
let ticket_id = SoulboundTicketContract::issue_ticket(
env.clone(),
event_id,
Address::from_string(&env, participant),
String::from_str(&env, &format!("Ticket #{}", i + 1))
)?;
println!("Issued ticket {} to {}", ticket_id, participant);
}
Ok(())
}fn purchase_ticket_with_escrow(
env: Env,
event_id: Symbol,
buyer: Address,
payment_amount: u128
) -> Result<Symbol, Box<dyn std::error::Error>> {
// 1. Create escrow for payment
let escrow_id = EscrowContract::create_escrow(
env.clone(),
buyer, // beneficiary (will be refunded if purchase fails)
payment_amount,
1704067200, // expiration
String::from_str(&env, "Ticket purchase escrow"),
1 // single confirmation needed
)?;
// 2. Fund the escrow
EscrowContract::fund_escrow(env.clone(), escrow_id)?;
// 3. Process ticket purchase through workflow
let workflow = EventTicketingWorkflow::new(env.clone());
let tx_id = workflow.process_ticket_purchase(
event_id,
buyer,
payment_amount
)?;
// 4. If successful, release funds to organizer
EscrowContract::release_funds(env, escrow_id)?;
Ok(tx_id)
}fn execute_multisig_payment(
env: Env,
destination: Address,
amount: u128,
owners: Vec<Address>
) -> Result<Symbol, Box<dyn std::error::Error>> {
// 1. Submit transaction
let tx_id = MultisigWalletContract::submit_transaction(
env.clone(),
destination,
amount,
String::from_str(&env, "Event payment"),
1704067200 + 86400 // expires in 24 hours
)?;
// 2. Collect approvals from required owners
let threshold = 2; // Assuming threshold is 2
let mut approvals = 0;
for owner in owners {
if approvals >= threshold {
break;
}
// In a real implementation, each owner would call this separately
let approved = MultisigWalletContract::approve_transaction(
env.clone(),
tx_id
)?;
if approved {
approvals += 1;
}
}
// 3. Execute the transaction if threshold met
if approvals >= threshold {
MultisigWalletContract::execute_transaction(env, tx_id)?;
println!("Transaction executed successfully");
} else {
println!("Insufficient approvals for transaction");
}
Ok(tx_id)
}- Input Validation: Always validate user inputs
- Access Control: Implement proper authorization checks
- Reentrancy Protection: Guard against reentrancy attacks
- Integer Overflow: Use safe arithmetic operations
- Event Logging: Emit events for important state changes
- Gas Efficiency: Optimize for low gas consumption
- Storage Patterns: Minimize storage operations
- Batch Operations: Combine multiple operations when possible
- Caching: Cache frequently accessed data
- Unit Tests: Test individual functions
- Integration Tests: Test contract interactions
- Property Tests: Test edge cases and invariants
- Gas Tests: Verify gas consumption limits
Extracted Soroban traces are persisted durably in the backend soroban_traces
table, deduplicated and transaction-ordered for the replay and verification
milestones.
Each row is uniquely identified by the source identity tuple
(transactionHash, ledgerSeq, eventIndex):
CREATE UNIQUE INDEX idx_soroban_traces_identity
ON soroban_traces(transactionHash, ledgerSeq, eventIndex);A write whose source identity is already present is a no-op, not an error: a retried job or a concurrent worker writing the same transaction produces exactly one row. The unique index backs this invariant even across process crashes.
| Column | Type | Meaning |
|---|---|---|
transactionHash |
text | Parent transaction hash (identity). |
ledgerSeq |
integer | Ledger sequence the transaction was applied on (identity). |
eventIndex |
integer | Position within the transaction's events array (identity). |
eventName |
text | Decoded LMS event name (e.g. course_created), queryable. |
eventPayload |
text (JSON) | Typed event fields, stored normalized (no XDR re-parse). |
rawXdr |
text | Raw base64 DiagnosticEvent, retained verbatim for audit/re-verify. |
successfulCall |
integer (bool) | Success classification of the enclosing contract call. |
applicationOrder |
integer | Application (Soroban) order within the transaction. |
createdAt / updatedAt |
text | Row timestamps. |
Traces from one transaction are returned strictly in applicationOrder (then
eventIndex), indistinguishable from on-chain emission order even after the
ingestion job is parallelized across worker coroutines.
The schema is created by the 003-create-soroban-traces migration (up/down) and
does not depend on synchronize: true. It is reached through the existing
migration runner (npm run migration:run / npm run migration:dry-run in
app/backend).
The backend exposes a task queue endpoint for ingesting L1 blockchain events. On the Stellar (Soroban) network it supports a dedicated Soroban trace extraction action that fetches a transaction's diagnostic events and emits typed, decodable trace records for the rest of the M1 pipeline to persist and re-verify.
Body:
{
"contractAddress": "string",
"eventName": "string",
"parameters": {
"transactionHash": "string"
},
"networkId": "stellar",
"priority": 0
}Fields:
networkId— must be"stellar"for Soroban extraction.action—"soroban-trace", the Soroban extraction action. EVM actions (listen/process/verify/index) are rejected on the Stellar network because Stellar speaks the Soroban protocol, not EVM JSON-RPC.parameters.transactionHash— the Soroban transaction hash whose diagnostic events should be extracted.
Behavior:
- A confirmed transaction yields one typed, serializable trace record per
events[]entry. Each record carries the typed event (type, contract id, base64 topics/value,inSuccessfulContractCall), the raw base64 wire entry, the parenttransactionHash, theledgersequence, and the SorobanapplicationOrder. - An unknown or not-yet-confirmed transaction is a transient, retryable condition: the job is retried and is never moved to the Dead Letter Queue on the first failure.
- An
events[]array or total encoded byte volume over the bounded resource policy (BLOCKCHAIN_MAX_BATCH_SIZE/BLOCKCHAIN_MAX_PAYLOAD_BYTES) is a permanentBlockchainPayloadLimitErrorand is Dead Letter Queued. - A malformed (invalid base64) event entry is a permanent, non-retryable input failure and is Dead Letter Queued.
Returns:
{ "success": true, "jobId": "string", "queueName": "blockchain-events", "timestamp": "ISO-8601" }The typed trace records are the ingest step every downstream M1 stage
(persistence, replay, re-verification) consumes; the Rust LmsEvent remains the
canonical typed payload for events known to the LMS contract.
This API documentation is maintained by the Gathera development team. For the most up-to-date information, please check the official repository.