Usage Guide
Learn how to use the Charli3 Oracle Data Verification (ODV) Client SDK to collect feed data and submit aggregation transactions.
Prerequisites
- SDK installed and configured (Setup Guide)
- Valid
config.yamlfile - Wallet funded with ~5 ADA for transaction fees
- Oracle network deployed and operational
CLI Usage
The SDK provides three main commands:
validate-config- Validate configurationfeeds- Collect oracle feed dataaggregate- Execute full aggregation workflow
Validate Configuration
Verify your configuration is correct before operations:
charli3 validate-config --config config.yamlOutput:
Configuration Validation
Setting Value Status
Network preprod Valid
Oracle Address addr_test1wp... Valid
Policy ID b00f27e5c228... Valid
Validity Length 120000ms Valid
Nodes Count 3 Valid
✓ Configuration is valid
Node Endpoints:
1. https://oracle-node-1.testnet.charli3.io
2. https://oracle-node-2.testnet.charli3.io
3. https://oracle-node-3.testnet.charli3.ioCollect Feed Data
Collect current oracle feed data from nodes without submitting a transaction:
charli3 feeds --config config.yamlBasic usage:
# Collect feeds with default settings
charli3 feeds --config config.yaml
# Save feed data to file for later use
charli3 feeds --config config.yaml --output feeds.json
# Verbose output showing all node responses
charli3 feeds --config config.yaml --verbose
# Override oracle policy ID
charli3 feeds --config config.yaml --policy-id <policy_id>
# Custom validity window (in milliseconds)
charli3 feeds --config config.yaml --validity-length 180000Output:
INFO: Collecting Oracle Feed Data
INFO: Policy ID: b00f27e5c2284f87b29c2b877dd341e3f0c3d06e1e0b02bb9c458f13
INFO: Validity window: 120000ms
INFO: Target nodes: 3
[Collecting feed updates...]
SUCCESS: Feed collection complete
Received: 3 responses
Aggregate feeds: 3
Calculated median: 1.234567With --verbose flag:
Node Feed Responses:
bd72e960d6237031290e8b4189ea7c6c305ef907d4881729db60e3f3b7c4af5b: 1.234500
7437e43e7e88c5642569f112f0e22b17c2fae155007254506dd0090dd9312568: 1.234600
76502e17aba981ecbf09fd5b41bc1b6a3f5a0a85bb47c07300b906a347969702: 1.234600Saved feed data format (feeds.json):
{
"node_messages": {
"bd72e960d6...": {
"message": "0123abcd...",
"signature": "4567efgh...",
"verification_key": "89abijkl..."
}
},
"aggregate_message": {
"node_feeds_count": 3,
"feeds": {
"007df380...": 1234567,
"b296714e...": 1234600
}
}
}Execute Aggregation Workflow
Submit an aggregation transaction to update on-chain oracle data:
charli3 aggregate --config config.yamlThe SDK automatically:
- Collects feed data from oracle nodes
- Builds aggregation transaction
- Collects signatures from nodes
- Prompts for submission confirmation
- Signs and submits transaction
- Waits for confirmation
Command options:
# Full aggregation with confirmation prompt
charli3 aggregate --config config.yaml
# Auto-submit without confirmation
charli3 aggregate --config config.yaml --auto-submit
# Use previously saved feed data
charli3 aggregate --config config.yaml --feed-data feeds.json
# Save transaction to specific file
charli3 aggregate --config config.yaml --output my_transaction.cbor
# Override wallet key
charli3 aggregate --config config.yaml --wallet-key payment.skey
# Verbose output showing all details
charli3 aggregate --config config.yaml --verboseTypical workflow output:
[Loading configuration...]
SUCCESS: Configuration and components initialized
Oracle Configuration:
Policy ID: b00f27e5c2284f87b29c2b877dd341e3f0c3d06e1e0b02bb9c458f13
Oracle Address: addr_test1wpgjxstaqc8am88c89zd72j5rw065ex8l5j7ln4pzngxt8qr4tzdj
Target nodes: 3
[Collecting feed data from oracle nodes...]
SUCCESS: Collected 3 feed responses
Node Feed Responses:
bd72e960d6...c4af5b: 1.234500
7437e43e7e...312568: 1.234600
76502e17ab...969702: 1.234600
[Building aggregation transaction...]
SUCCESS: Transaction constructed
Transaction ID: a1b2c3d4e5f6...
Median value: 1.234567
[Collecting signatures from oracle nodes...]
SUCCESS: Collected 3 signatures
INFO: Attaching signatures to transaction
SUCCESS: Transaction saved to odv_transaction_b00f27e5.cbor
┌─ Transaction Preview ─────────────────────────────────────┐
│ │
│ Transaction Ready for Submission │
│ │
│ Transaction ID: │
│ a1b2c3d4e5f67890... │
│ │
│ Oracle Feed Details: │
│ • Median Value: 1.234567 │
│ • Node Responses: 3 │
│ • Signatures Collected: 3 │
│ │
│ Transaction Details: │
│ • Saved to: odv_transaction_b00f27e5.cbor │
│ │
│ ⚠️ This action will submit the transaction to the │
│ blockchain │
│ │
└────────────────────────────────────────────────────────────┘
Submit transaction to blockchain? [Y/n]: After confirmation:
[Submitting transaction to blockchain...]
SUCCESS: Transaction confirmed on blockchain
┌─ Aggregation Summary ──────────────────────────────────────┐
│ │
│ ODV Aggregation Complete │
│ │
│ Transaction ID: │
│ a1b2c3d4e5f67890... │
│ │
│ Median Value: 1.234567 │
│ Node Responses: 3 │
│ Signatures: 3 │
│ │
└─────────────────────────────────────────────────────────────┘Auto-submit mode (--auto-submit):
charli3 aggregate --config config.yaml --auto-submitSkips confirmation prompt and immediately submits the transaction.
Using saved feed data:
# First, collect and save feeds
charli3 feeds --config config.yaml --output feeds.json
# Later, use saved data for aggregation
charli3 aggregate --config config.yaml --feed-data feeds.jsonThis is useful for:
- Testing transaction building without fresh data collection
- Reusing feed data if initial submission failed
- Debugging aggregation logic
Transaction Output Files
All transactions are saved to .cbor files:
Default naming:
odv_transaction_<policy_id_prefix>.cborManual submission:
# Using cardano-cli
cardano-cli transaction submit \
--tx-file odv_transaction_b00f27e5.cbor \
--testnet-magic 1
# Or via block explorer
# Copy CBOR content and submit via web interfaceProgrammatic Usage
Integrate ODV Client SDK directly into your Python applications:
Basic Integration
import asyncio
from pathlib import Path
from charli3_odv_client.config import ODVClientConfig
from charli3_odv_client.core.client import ODVClient
from charli3_odv_client.models.requests import OdvFeedRequest
from charli3_odv_client.models.base import TxValidityInterval
async def collect_feeds():
# Load configuration
config = ODVClientConfig.from_yaml(Path("config.yaml"))
# Initialize client
client = ODVClient()
# Create feed request
feed_request = OdvFeedRequest(
oracle_nft_policy_id=config.policy_id,
tx_validity_interval=TxValidityInterval(
start=1640000000000, # POSIX ms
end=1640000120000
)
)
# Collect feeds from nodes
node_messages = await client.collect_feed_updates(
nodes=config.nodes,
feed_request=feed_request
)
print(f"Collected {len(node_messages)} feed responses")
for pub_key, msg in node_messages.items():
feed_value = msg.message.feed / 1_000_000
print(f"Node {pub_key[:16]}...: {feed_value:.6f}")
return node_messages
# Run
asyncio.run(collect_feeds())Full Aggregation Workflow
import asyncio
from pathlib import Path
from charli3_odv_client.config import ODVClientConfig, ReferenceScriptConfig, KeyManager
from charli3_odv_client.core.client import ODVClient
from charli3_odv_client.models.requests import OdvFeedRequest
from charli3_odv_client.cli.utils.shared import (
create_chain_query,
setup_transaction_builder
)
async def submit_aggregation():
# Load configuration
config = ODVClientConfig.from_yaml(Path("config.yaml"))
ref_script_config = ReferenceScriptConfig.from_yaml(Path("config.yaml"))
# Initialize components
client = ODVClient()
chain_query = create_chain_query(config)
tx_manager, tx_builder = setup_transaction_builder(
config, ref_script_config, chain_query
)
# Load wallet
signing_key, _, _, change_address = KeyManager.load_from_config(
config.wallet
)
# Calculate validity window
validity_window = tx_manager.calculate_validity_window(
config.odv_validity_length
)
# Step 1: Collect feeds
feed_request = OdvFeedRequest(
oracle_nft_policy_id=config.policy_id,
tx_validity_interval=TxValidityInterval(
start=validity_window.validity_start,
end=validity_window.validity_end
)
)
node_messages = await client.collect_feed_updates(
nodes=config.nodes,
feed_request=feed_request
)
if not node_messages:
raise Exception("No valid node responses received")
print(f"Collected {len(node_messages)} feed responses")
# Step 2: Build transaction
odv_result = await tx_builder.build_odv_tx(
node_messages=node_messages,
signing_key=signing_key,
change_address=change_address,
validity_window=validity_window
)
print(f"Transaction built: {odv_result.transaction.id}")
print(f"Median value: {odv_result.median_value / 1_000_000:.6f}")
# Step 3: Collect signatures
from charli3_odv_client.models.requests import OdvTxSignatureRequest
tx_request = OdvTxSignatureRequest(
node_messages=node_messages,
tx_body_cbor=odv_result.transaction.transaction_body.to_cbor_hex()
)
signatures = await client.collect_tx_signatures(
nodes=config.nodes,
tx_request=tx_request
)
if not signatures:
raise Exception("No valid signatures received")
print(f"Collected {len(signatures)} signatures")
# Step 4: Attach signatures
odv_result.transaction = client.attach_signature_witnesses(
original_tx=odv_result.transaction,
signatures=signatures,
node_messages=node_messages
)
# Step 5: Submit transaction
status, submitted_tx = await tx_manager.sign_and_submit(
odv_result.transaction,
signing_keys=[signing_key],
wait_confirmation=True
)
if status == "confirmed":
print(f"Transaction confirmed: {submitted_tx.id}")
return submitted_tx
else:
raise Exception(f"Transaction failed with status: {status}")
# Run
asyncio.run(submit_aggregation())Custom Feed Collection
import asyncio
from charli3_odv_client.config import ODVClientConfig, NodeConfig
from charli3_odv_client.core.client import ODVClient
from charli3_odv_client.models.requests import OdvFeedRequest
from charli3_odv_client.models.base import TxValidityInterval
async def collect_from_specific_nodes():
# Create custom node configuration
nodes = [
NodeConfig(
root_url="https://oracle-node-1.testnet.charli3.io",
pub_key="bd72e960d6237031290e8b4189ea7c6c305ef907d4881729db60e3f3b7c4af5b"
),
NodeConfig(
root_url="https://oracle-node-2.testnet.charli3.io",
pub_key="7437e43e7e88c5642569f112f0e22b17c2fae155007254506dd0090dd9312568"
)
]
# Initialize client
client = ODVClient()
# Create request
feed_request = OdvFeedRequest(
oracle_nft_policy_id="b00f27e5c2284f87b29c2b877dd341e3f0c3d06e1e0b02bb9c458f13",
tx_validity_interval=TxValidityInterval(
start=1640000000000,
end=1640000120000
)
)
# Collect feeds
node_messages = await client.collect_feed_updates(
nodes=nodes,
feed_request=feed_request
)
return node_messages
# Run
asyncio.run(collect_from_specific_nodes())Calculate Aggregate Statistics
from charli3_odv_client.core.aggregation import (
build_aggregate_message,
calculate_median
)
def analyze_feed_data(node_messages):
# Build aggregate message
aggregate_message = build_aggregate_message(
list(node_messages.values())
)
# Get feed values
feeds = [msg.message.feed for msg in node_messages.values()]
# Calculate median
median_value = calculate_median(feeds)
print(f"Total nodes: {aggregate_message.node_feeds_count}")
print(f"Median value: {median_value / 1_000_000:.6f}")
print(f"Min value: {min(feeds) / 1_000_000:.6f}")
print(f"Max value: {max(feeds) / 1_000_000:.6f}")
return aggregate_messageError Handling
import asyncio
from charli3_odv_client.exceptions import (
NetworkError,
ValidationError,
OracleTransactionError,
StateValidationError
)
async def robust_aggregation():
try:
# Your aggregation logic here
pass
except NetworkError as e:
print(f"Network error: {e}")
print("Check node connectivity and API endpoints")
except StateValidationError as e:
print(f"State validation error: {e}")
print("Oracle state may not be ready for aggregation")
except ValidationError as e:
print(f"Validation error: {e}")
print("Check configuration parameters")
except OracleTransactionError as e:
print(f"Transaction error: {e}")
print("Review transaction parameters and try again")
except Exception as e:
print(f"Unexpected error: {e}")
raiseIntegration Patterns
Scheduled Aggregations
import asyncio
import schedule
import time
from datetime import datetime
async def scheduled_aggregation():
print(f"[{datetime.now()}] Running scheduled aggregation")
try:
# Your aggregation logic
await submit_aggregation()
print(f"[{datetime.now()}] Aggregation successful")
except Exception as e:
print(f"[{datetime.now()}] Aggregation failed: {e}")
def run_scheduler():
# Schedule aggregation every hour
schedule.every().hour.at(":00").do(
lambda: asyncio.run(scheduled_aggregation())
)
while True:
schedule.run_pending()
time.sleep(60)
# Run scheduler
run_scheduler()Conditional Aggregation
async def conditional_aggregation(threshold_change=0.01):
"""
Submit aggregation only if median value changed significantly.
"""
config = ODVClientConfig.from_yaml(Path("config.yaml"))
client = ODVClient()
# Get current on-chain value
chain_query = create_chain_query(config)
# ... query current oracle state
current_value = 1.234567 # Retrieved from chain
# Collect new feeds
validity_window = calculate_validity_window(config.odv_validity_length)
feed_request = OdvFeedRequest(
oracle_nft_policy_id=config.policy_id,
tx_validity_interval=TxValidityInterval(
start=validity_window.validity_start,
end=validity_window.validity_end
)
)
node_messages = await client.collect_feed_updates(
nodes=config.nodes,
feed_request=feed_request
)
# Calculate new median
feeds = [msg.message.feed for msg in node_messages.values()]
new_median = calculate_median(feeds) / 1_000_000
# Check if change exceeds threshold
change = abs(new_median - current_value) / current_value
if change > threshold_change:
print(f"Significant change detected: {change:.2%}")
print(f"Submitting aggregation: {current_value} → {new_median}")
# Submit aggregation
await submit_aggregation()
else:
print(f"Change below threshold: {change:.2%}")
print("Skipping aggregation")Common Workflows
1. One-Time Aggregation
# Validate configuration
charli3 validate-config --config config.yaml
# Submit aggregation with confirmation
charli3 aggregate --config config.yaml2. Automated Aggregations
# Save as aggregation script
cat > aggregate.sh << 'EOF'
#!/bin/bash
charli3 aggregate \
--config /path/to/config.yaml \
--auto-submit \
--output "/logs/tx_$(date +%Y%m%d_%H%M%S).cbor"
EOF
chmod +x aggregate.sh
# Schedule with cron (every hour)
crontab -e
# Add: 0 * * * * /path/to/aggregate.sh >> /logs/aggregation.log 2>&1Troubleshooting
No Node Responses
Symptoms:
ERROR: Feed collection failed
Received: 0 responsesSolutions:
- Verify node URLs are accessible:
curl https://oracle-node-1.testnet.charli3.io/health - Check node public keys match configuration
- Ensure validity window is reasonable (not in past/future)
- Verify oracle is operational on-chain
Insufficient Signatures
Symptoms:
ERROR: No valid signatures receivedSolutions:
- Verify nodes can access transaction body
- Check node public keys match oracle deployment
- Ensure transaction is valid (check node logs)
- Verify validity window overlaps with node response times
Transaction Building Failed
Symptoms:
ERROR: Failed to build ODV transactionSolutions:
- Check oracle has available UTXO pairs:
# Query oracle address for UTXOs cardano-cli query utxo --address <oracle_address> - Verify reference script exists and is accessible
- Ensure wallet has sufficient funds
- Check oracle isn’t paused or in closing state
State Validation Error
Symptoms:
ERROR: No empty transport UTxO foundSolutions:
- Wait for current aggregations to complete
- Check if oracle needs reward processing
- Verify oracle is deployed and operational
- Scale up oracle capacity if needed
Network Connectivity Issues
Symptoms:
ERROR: Network error: Connection refusedSolutions:
-
For Blockfrost:
- Verify project ID is correct
- Check API rate limits
- Test API connectivity:
curl -H "project_id: <id>" https://cardano-preprod.blockfrost.io/api/v0/health
-
For Ogmios/Kupo:
- Verify services are running:
curl http://localhost:1442/health - Check ports are accessible
- Ensure correct network (testnet/mainnet)
- Verify services are running:
Transaction Submission Failed
Symptoms:
ERROR: Transaction submission failed
Status: failedSolutions:
- Check wallet has sufficient ADA
- Verify transaction isn’t expired
- Ensure reference script is accessible
- Check oracle state is valid for aggregation
- Review node logs for rejection reasons
Best Practices
1. Configuration Management
- Use environment variables for sensitive data
- Version control config templates (without secrets)
- Separate configs per environment (dev/staging/prod)
- Document custom settings and their purposes
2. Error Recovery
- Save transaction CBORs before submission
- Log all operations with timestamps
- Implement retry logic with exponential backoff
- Monitor for failed aggregations and alert
3. Cost Optimization
- Use reference scripts to reduce tx sizes
- Batch operations when possible
- Monitor ADA consumption
- Set up alerts for low wallet balance
4. Monitoring
- Track feed collection success rates
- Monitor signature collection latency
- Log transaction submissions and confirmations
- Alert on failures or anomalies
5. Security
- Rotate wallet periodically
- Limit funds in hot wallets
- Audit configurations regularly
- Use read-only wallets for monitoring
Advanced Usage
Custom Validity Windows
from charli3_odv_client.blockchain.transactions import ValidityWindow
# Create custom validity window
validity_window = ValidityWindow(
validity_start=1640000000000, # Custom start time
validity_end=1640000120000, # Custom end time (must be start + validity_length)
current_time=1640000060000 # Current time for calculations
)
# Use in aggregation
odv_result = await tx_builder.build_odv_tx(
node_messages=node_messages,
signing_key=signing_key,
change_address=change_address,
validity_window=validity_window
)Transaction Fee Estimation
# Build transaction
odv_result = await tx_builder.build_odv_tx(...)
# Get transaction size
tx_size = len(odv_result.transaction.to_cbor())
print(f"Transaction size: {tx_size} bytes")
# Estimate execution units
execution_units = await tx_manager.estimate_execution_units(
odv_result.transaction
)
print(f"Execution units: {execution_units}")Parallel Feed Collection
import asyncio
async def collect_from_multiple_oracles(configs):
"""Collect feeds from multiple oracles in parallel."""
tasks = [
collect_feeds_for_oracle(config)
for config in configs
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for config, result in zip(configs, results):
if isinstance(result, Exception):
print(f"Failed for {config.policy_id}: {result}")
else:
print(f"Success for {config.policy_id}: {len(result)} feeds")
return results