Skip to content

Latest commit

 

History

History
1270 lines (999 loc) · 35.5 KB

File metadata and controls

1270 lines (999 loc) · 35.5 KB

Gathera API Documentation

This document provides comprehensive API documentation for all Gathera smart contracts, including function signatures, parameters, return values, usage examples, and gas costs.

Table of Contents

  1. Overview
  2. Ticket Contract API
  3. Escrow Contract API
  4. Multi-Signature Wallet Contract API
  5. Integration Layer API
  6. Decode Error Taxonomy and Retry Mapping
  7. Soroban Trace Replay API
  8. Soroban Verification API
  9. Error Handling
  10. Gas Cost Analysis
  11. Usage Examples
  12. Soroban Trace Store Schema
  13. Blockchain Event Task Queue

Overview

The Gathera platform consists of three main smart contracts:

  1. SoulboundTicketContract: Manages non-transferable event tickets
  2. EscrowContract: Handles secure fund escrow with dispute resolution
  3. MultisigWalletContract: Provides multi-signature wallet functionality

All contracts are built on the Stellar Soroban platform using Rust and compile to WebAssembly.

Ticket Contract API

Contract Address

[TBD] (To be populated after deployment)

Core Functions

issue_ticket

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 environment
  • event_id (Symbol): Unique identifier for the event
  • recipient (Address): Stellar address of the ticket recipient
  • metadata (String): Additional ticket metadata (JSON format)

Returns:

  • Result<Symbol, TicketError>:
    • Ok(Symbol): Unique ticket identifier
    • Err(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
)?;

verify_ownership

Verifies if a given address owns a specific ticket.

pub fn verify_ownership(
    env: Env,
    ticket_id: Symbol,
    claimed_owner: Address,
) -> bool

Parameters:

  • env (Env): The contract environment
  • ticket_id (Symbol): Unique identifier for the ticket
  • claimed_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
);

get_ticket

Retrieves ticket information.

pub fn get_ticket(env: Env, ticket_id: Symbol) -> Result<Ticket, TicketError>

Parameters:

  • env (Env): The contract environment
  • ticket_id (Symbol): Unique identifier for the ticket

Returns:

  • Result<Ticket, TicketError>:
    • Ok(Ticket): Ticket data structure
    • Err(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);

Data Structures

Ticket

pub struct Ticket {
    pub ticket_id: Symbol,
    pub event_id: Symbol,
    pub owner: Address,
    pub issued_at: u64,
    pub metadata: String,
}

TicketError

pub enum TicketError {
    TicketAlreadyExists = 1,
    TicketNotFound = 2,
    Unauthorized = 3,
    InvalidEventId = 4,
    NotTransferable = 5,
    EventEnded = 6,
    MaxTicketsReached = 7,
    NotImplemented = 255,
}

Escrow Contract API

Contract Address

[TBD] (To be populated after deployment)

Core Functions

create_escrow

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 environment
  • beneficiary (Address): Address of the beneficiary
  • amount (u128): Amount to escrow (in stroops)
  • expires_at (u64): Expiration timestamp (Unix timestamp)
  • terms (String): Escrow terms and conditions
  • required_confirmations (u32): Number of confirmations needed for release

Returns:

  • Result<Symbol, EscrowError>:
    • Ok(Symbol): Unique escrow identifier
    • Err(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
)?;

fund_escrow

Funds an existing escrow with tokens.

pub fn fund_escrow(env: Env, escrow_id: Symbol) -> Result<bool, EscrowError>

Parameters:

  • env (Env): The contract environment
  • escrow_id (Symbol): Unique identifier for the escrow

Returns:

  • Result<bool, EscrowError>:
    • Ok(bool): True if funding was successful
    • Err(EscrowError): Error if funding fails

Gas Cost: ~10,000 gas units + token transfer cost

Example:

let success = EscrowContract::fund_escrow(env, escrow_id)?;

release_funds

Releases funds from escrow to the beneficiary.

pub fn release_funds(env: Env, escrow_id: Symbol) -> Result<bool, EscrowError>

Parameters:

  • env (Env): The contract environment
  • escrow_id (Symbol): Unique identifier for the escrow

Returns:

  • Result<bool, EscrowError>:
    • Ok(bool): True if release was successful
    • Err(EscrowError): Error if release fails

Gas Cost: ~15,000 gas units + token transfer cost

Example:

let success = EscrowContract::release_funds(env, escrow_id)?;

create_dispute

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 environment
  • escrow_id (Symbol): Unique identifier for the escrow
  • reason (String): Dispute reason description

Returns:

  • Result<Symbol, EscrowError>:
    • Ok(Symbol): Unique dispute identifier
    • Err(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)?;

resolve_dispute

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 environment
  • dispute_id (Symbol): Unique identifier for the dispute
  • resolution (String): Dispute resolution details

Returns:

  • Result<bool, EscrowError>:
    • Ok(bool): True if resolution was successful
    • Err(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)?;

get_escrow

Retrieves escrow information.

pub fn get_escrow(env: Env, escrow_id: Symbol) -> Result<Escrow, EscrowError>

Parameters:

  • env (Env): The contract environment
  • escrow_id (Symbol): Unique identifier for the escrow

Returns:

  • Result<Escrow, EscrowError>:
    • Ok(Escrow): Escrow data structure
    • Err(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);

Data Structures

Escrow

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>,
}

EscrowStatus

pub enum EscrowStatus {
    Pending = 0,
    Funded = 1,
    Completed = 2,
    Disputed = 3,
    Refunded = 4,
    Expired = 5,
}

Dispute

pub struct Dispute {
    pub dispute_id: Symbol,
    pub escrow_id: Symbol,
    pub initiator: Address,
    pub reason: String,
    pub resolved: bool,
    pub resolution: Option<String>,
}

EscrowError

pub enum EscrowError {
    EscrowAlreadyExists = 1,
    EscrowNotFound = 2,
    Unauthorized = 3,
    InsufficientFunds = 4,
    InvalidTerms = 5,
    AlreadyCompleted = 6,
    DisputeExists = 7,
    InvalidResolution = 8,
    EscrowExpired = 9,
    NotImplemented = 255,
}

Multi-Signature Wallet Contract API

Contract Address

[TBD] (To be populated after deployment)

Core Functions

initialize

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 environment
  • owners (Vec): List of initial wallet owners
  • threshold (u32): Number of signatures required (1 ≤ threshold ≤ owners.len())
  • timelock (u64): Time-lock period in seconds
  • max_amount (u128): Maximum transaction amount

Returns:

  • Result<bool, MultisigError>:
    • Ok(bool): True if initialization was successful
    • Err(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
)?;

submit_transaction

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 environment
  • destination (Address): Recipient address
  • amount (u128): Amount to transfer (in stroops)
  • data (String): Transaction data/payload
  • expires_at (u64): Expiration timestamp

Returns:

  • Result<Symbol, MultisigError>:
    • Ok(Symbol): Unique transaction identifier
    • Err(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
)?;

approve_transaction

Approves a pending transaction.

pub fn approve_transaction(
    env: Env,
    transaction_id: Symbol,
) -> Result<bool, MultisigError>

Parameters:

  • env (Env): The contract environment
  • transaction_id (Symbol): Unique identifier for the transaction

Returns:

  • Result<bool, MultisigError>:
    • Ok(bool): True if approval was successful
    • Err(MultisigError): Error if approval fails

Gas Cost: ~10,000 gas units

Example:

let success = MultisigWalletContract::approve_transaction(env, tx_id)?;

execute_transaction

Executes an approved transaction.

pub fn execute_transaction(
    env: Env,
    transaction_id: Symbol,
) -> Result<bool, MultisigError>

Parameters:

  • env (Env): The contract environment
  • transaction_id (Symbol): Unique identifier for the transaction

Returns:

  • Result<bool, MultisigError>:
    • Ok(bool): True if execution was successful
    • Err(MultisigError): Error if execution fails

Gas Cost: ~15,000 gas units + token transfer cost

Example:

let success = MultisigWalletContract::execute_transaction(env, tx_id)?;

add_owner

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 environment
  • new_owner (Address): Address of the new owner
  • transaction_id (Symbol): Governing transaction ID

Returns:

  • Result<bool, MultisigError>:
    • Ok(bool): True if owner addition was successful
    • Err(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)?;

remove_owner

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 environment
  • owner_to_remove (Address): Address of the owner to remove
  • transaction_id (Symbol): Governing transaction ID

Returns:

  • Result<bool, MultisigError>:
    • Ok(bool): True if owner removal was successful
    • Err(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)?;

change_threshold

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 environment
  • new_threshold (u32): New threshold value
  • transaction_id (Symbol): Governing transaction ID

Returns:

  • Result<bool, MultisigError>:
    • Ok(bool): True if threshold change was successful
    • Err(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)?;

get_transaction

Retrieves transaction information.

pub fn get_transaction(
    env: Env,
    transaction_id: Symbol,
) -> Result<Transaction, MultisigError>

Parameters:

  • env (Env): The contract environment
  • transaction_id (Symbol): Unique identifier for the transaction

Returns:

  • Result<Transaction, MultisigError>:
    • Ok(Transaction): Transaction data structure
    • Err(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);

get_config

Retrieves current wallet configuration.

pub fn get_config(env: Env) -> MultisigConfig

Parameters:

  • 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);

Data Structures

Transaction

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,
}

TransactionStatus

pub enum TransactionStatus {
    Pending = 0,
    Approved = 1,
    Rejected = 2,
    Executed = 3,
    Expired = 4,
}

MultisigConfig

pub struct MultisigConfig {
    pub owners: Vec<Address>,
    pub threshold: u32,
    pub timelock: u64,
    pub max_transaction_amount: u128,
}

MultisigError

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,
}

Integration Layer API

Core Functions

EventTicketingWorkflow::create_event_with_escrow

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 event
  • organizer (Address): Event organizer address
  • ticket_price (u128): Price per ticket
  • max_tickets (u32): Maximum number of tickets
  • escrow_terms (String): Escrow terms and conditions

Returns:

  • Result<Symbol, WorkflowError>:
    • Ok(Symbol): Workflow transaction identifier
    • Err(WorkflowError): Error if workflow fails

Gas Cost: ~50,000 gas units

EventTicketingWorkflow::process_ticket_purchase

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 event
  • buyer (Address): Buyer address
  • payment_amount (u128): Payment amount

Returns:

  • Result<Symbol, WorkflowError>:
    • Ok(Symbol): Transaction identifier
    • Err(WorkflowError): Error if purchase fails

Gas Cost: ~35,000 gas units

GatheraClient::get_addresses

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

Decode Error Taxonomy and Retry Mapping

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.

Soroban Trace Replay API

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.

POST /api/task-queue/blockchain-event/replay

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 + 1 with no gaps or duplicates.
  • Overlap-safe — concurrent live extraction and backfill absorb one another; the union is stored exactly once.

Internal components

  • soroban:replay BullMQ queue — carries replay job payloads { networkId, fromSeq, toSeq, batchSize }.
  • SorobanReplayService.runBackfill — walks [fromSeq, toSeq], fetches events per ledger, ingests each trace through the SorobanTraceIngestPort, and advances the cursor atomically per batch.
  • ReplayCursorServicegetCursor, resumeFrom, advanceWithBatch (transactional upsert; never regresses the cursor).
  • StellarProvider.getLatestLedger / eventsForLedger — Soroban RPC head query and per-ledger event extraction.

Soroban Verification API

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 taxonomy

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).

Request

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.

Response

{
  "outcome": "verified",
  "verified": true,
  "transactionHash": "0x…",
  "ledgerSeq": 42,
  "eventName": "course_created"
}

Durable record

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).

Error Handling

All contracts use comprehensive error handling with specific error codes:

Common Error Patterns

  1. Authorization Errors: Verify caller permissions
  2. Validation Errors: Check input parameters
  3. State Errors: Verify contract state
  4. Business Logic Errors: Enforce business rules

Error Recovery

  • Check error codes for specific failure reasons
  • Use appropriate retry strategies for temporary failures
  • Implement proper error logging and monitoring

Gas Cost Analysis

Cost Summary (in gas units)

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

Optimization Tips

  1. Batch Operations: Combine multiple operations when possible
  2. Metadata Optimization: Keep metadata strings concise
  3. Storage Patterns: Optimize data storage layouts
  4. Event Logging: Use events instead of storage when possible

Usage Examples

Complete Event Setup Workflow

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

Ticket Purchase with Escrow

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

Multi-Signature Transaction Flow

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

Best Practices

Security Considerations

  1. Input Validation: Always validate user inputs
  2. Access Control: Implement proper authorization checks
  3. Reentrancy Protection: Guard against reentrancy attacks
  4. Integer Overflow: Use safe arithmetic operations
  5. Event Logging: Emit events for important state changes

Performance Optimization

  1. Gas Efficiency: Optimize for low gas consumption
  2. Storage Patterns: Minimize storage operations
  3. Batch Operations: Combine multiple operations when possible
  4. Caching: Cache frequently accessed data

Testing Strategy

  1. Unit Tests: Test individual functions
  2. Integration Tests: Test contract interactions
  3. Property Tests: Test edge cases and invariants
  4. Gas Tests: Verify gas consumption limits

Soroban Trace Store Schema

Extracted Soroban traces are persisted durably in the backend soroban_traces table, deduplicated and transaction-ordered for the replay and verification milestones.

Identity key and deduplication

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.

Columns

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.

Ordering guarantee

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).

Blockchain Event Task Queue

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.

POST /api/task-queue/blockchain-event

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 parent transactionHash, the ledger sequence, and the Soroban applicationOrder.
  • 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 permanent BlockchainPayloadLimitError and 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.