Skip to content

feat: Add typed decoder utilities for contract return values#288

Merged
manoahLinks merged 2 commits into
crowdpass-live:mainfrom
rahimatonize:feature/typed-decoder-utilities
Apr 27, 2026
Merged

feat: Add typed decoder utilities for contract return values#288
manoahLinks merged 2 commits into
crowdpass-live:mainfrom
rahimatonize:feature/typed-decoder-utilities

Conversation

@rahimatonize
Copy link
Copy Markdown
Contributor

Add Typed Decoder Utilities for Contract Return Values
🎯 Overview
Implements a comprehensive typed decoder library for safely parsing and validating Soroban contract return values in TypeScript. This enhancement eliminates unsafe type assertions and provides explicit validation with clear error messages.

🚀 What's New
Core Features
30+ Decoder Functions: Complete library for type-safe parsing
Composable Architecture: Build complex decoders from simple primitives
Type Safety: Full TypeScript support with type inference
Clear Error Messages: Detailed error context for debugging
Safe Decoding: Optional result-based decoding without exceptions
Pre-built Contract Decoders: Ready-to-use decoders for common contract types
Decoder Categories
Primitive Decoders
decodeString, decodeNumber, decodeBigInt, decodeBoolean
decodeBytes, decodeAddress, decodeSymbol
Composite Decoders
decodeArray / decodeVec - Arrays with element validation
decodeTuple - Fixed-length tuples with mixed types
decodeStruct - Objects with field validation
decodeMap - Key-value maps
decodeOption - Optional values (handles Soroban Option)
Soroban-Specific Decoders
Unsigned integers: decodeU32, decodeU64, decodeU128
Signed integers: decodeI32, decodeI64, decodeI128
Fixed bytes: decodeBytesN(size)
Unit type: decodeVoid
Utility Decoders
decodeTransform - Decode and transform values
decodeValidate - Decode with custom validation
decodeWithDefault - Decode with fallback values
decodeOneOf - Try multiple decoders
decodeEnum - Validate enum values
decodeLiteral - Match exact values
Contract Decoders
ContractDecoder.event() - Event struct
ContractDecoder.ticketTier() - TicketTier struct
ContractDecoder.buyerPurchase() - BuyerPurchase struct
ContractDecoder.tbaToken() - TBA token tuple
📝 Changes
New Files
decoders.ts
(650 lines)

Complete decoder library implementation
DecoderError class for structured errors
Safe decoding utilities
Contract-specific decoders
decoders.test.ts
(450 lines)

Comprehensive test suite with 50+ test cases
Tests for all decoder types
Error handling validation
Edge case coverage
DECODERS.md (800 lines)

Complete documentation
Usage examples for all decoders
Best practices guide
API reference
Troubleshooting guide
decoder-usage.ts
(500 lines)

15 practical examples
Real-world usage patterns
Integration with SDK
Modified Files
index.ts
: Added decoder exports to public API
README.md: Added decoder utilities documentation
💻 Usage
Basic Usage
import { decodeString, decodeNumber, decodeArray } from "@crowdpass/tokenbound-sdk";

// Decode primitives
const name = decodeString("Alice"); // "Alice"
const age = decodeNumber(25); // 25

// Decode arrays
const ids = decodeArray(decodeNumber)([1, 2, 3]);
// [1, 2, 3]
Soroban Types
import { decodeU32, decodeU128, decodeI128, decodeAddress } from "@crowdpass/tokenbound-sdk";

// Unsigned integers
const eventId = decodeU32(123);
const totalTickets = decodeU128(1000n);

// Signed integers
const price = decodeI128(5000000000n);

// Addresses
const organizer = decodeAddress("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
Structs and Complex Types
import { decodeStruct, decodeU32, decodeString, decodeBoolean } from "@crowdpass/tokenbound-sdk";

// Define struct decoder
const decodeUser = decodeStruct({
id: decodeU32,
name: decodeString,
email: decodeString,
active: decodeBoolean,
});

// Use it
const user = decodeUser({
id: 1,
name: "Alice",
email: "alice@example.com",
active: true,
});
Contract Decoders
import { ContractDecoder } from "@crowdpass/tokenbound-sdk";

// Decode event response
const event = ContractDecoder.event()({
id: 1,
theme: "Web3 Conference",
organizer: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
event_type: "Conference",
total_tickets: 500n,
tickets_sold: 250n,
ticket_price: 1000000000n,
start_date: 1234567890,
end_date: 1234567900,
is_canceled: false,
ticket_nft_addr: "CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
payment_token: "CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
});

// Decode array of tiers
const tiers = decodeArray(ContractDecoder.ticketTier())(rawTiers);
Advanced Composition
import {
decodeStruct,
decodeArray,
decodeOption,
decodeU32,
decodeString,
decodeI128,
} from "@crowdpass/tokenbound-sdk";

// Compose complex decoders
const decodeEventWithTiers = decodeStruct({
id: decodeU32,
name: decodeString,
tiers: decodeArray(
decodeStruct({
name: decodeString,
price: decodeI128,
available: decodeOption(decodeU32),
})
),
});

const event = decodeEventWithTiers({
id: 1,
name: "Conference",
tiers: [
{ name: "General", price: 1000000000n, available: 100 },
{ name: "VIP", price: 5000000000n, available: null },
],
});
Transform and Validate
import { decodeTransform, decodeValidate, decodeI128, decodeString } from "@crowdpass/tokenbound-sdk";

// Transform decoded value
const decodeUppercase = decodeTransform(
decodeString,
(s) => s.toUpperCase()
);

const theme = decodeUppercase("web3 conference");
// "WEB3 CONFERENCE"

// Validate decoded value
const decodePositivePrice = decodeValidate(
decodeI128,
(price) => price > 0n,
"Price must be positive"
);

const price = decodePositivePrice(1000000000n);
// 1000000000n (validated)
Safe Decoding
import { safeDecode, decodeNumber } from "@crowdpass/tokenbound-sdk";

// Returns result object instead of throwing
const result = safeDecode(decodeNumber, "invalid");

if (result.success) {
console.log("Value:", result.value);
} else {
console.error("Error:", result.error.message);
}
Integration with SDK
import { createTokenboundSdk, ContractDecoder } from "@crowdpass/tokenbound-sdk";

const sdk = createTokenboundSdk({
// ... config
});

// Get raw response
const rawEvent = await sdk.eventManager.getEvent(1);

// Decode with explicit validation
const event = ContractDecoder.event()(rawEvent);

// Now you have type-safe, validated data
console.log(event.theme); // string
console.log(event.total_tickets); // bigint
console.log(event.is_canceled); // boolean
🎯 Benefits
For Developers
✅ Type Safety - Compile-time and runtime type checking
✅ Clear Errors - Detailed error messages with context
✅ Composable - Build complex decoders from simple ones
✅ Reusable - Define once, use everywhere
✅ Testable - Easy to test with mock data

For Code Quality
✅ No Unsafe Casts - Eliminates as type assertions
✅ Explicit Validation - Clear data validation logic
✅ Early Detection - Catch errors at parse time
✅ Self-Documenting - Decoders document expected structure

For Maintenance
✅ Centralized Logic - Single source of truth for parsing
✅ Easy Updates - Change decoder, update everywhere
✅ Refactor-Friendly - TypeScript catches breaking changes

🧪 Testing
cd soroban-client/sdk
npm test -- decoders.test.ts
Test Coverage:

✅ Primitive decoders (10+ tests)
✅ Composite decoders (15+ tests)
✅ Soroban-specific decoders (10+ tests)
✅ Utility decoders (10+ tests)
✅ Contract decoders (5+ tests)
✅ Error handling (10+ tests)
📚 Documentation
DECODERS.md: Complete documentation (800+ lines)
examples/decoder-usage.ts: 15 practical examples (500+ lines)
README.md: Updated with decoder info
🔄 Migration Guide
From Unsafe Type Assertions
Before:

const event = response as EventRecord;
// No validation, runtime errors possible
// TypeScript can't help if structure changes
After:

const event = ContractDecoder.event()(response);
// ✅ Validated at runtime
// ✅ Type-safe
// ✅ Clear error messages
// ✅ Catches structure mismatches
From Manual Validation
Before:

function parseEvent(raw: any): EventRecord {
if (typeof raw.id !== "number") {
throw new Error("Invalid id");
}
if (typeof raw.theme !== "string") {
throw new Error("Invalid theme");
}
// ... 20+ more checks
return raw as EventRecord;
}
After:

const decodeEvent = ContractDecoder.event();
const event = decodeEvent(raw);
// All validation handled automatically
✅ Checklist
Implementation complete (650+ lines)
Comprehensive tests (50+ test cases)
Documentation written (800+ lines)
Usage examples provided (15 examples)
README updated
Backward compatible (no breaking changes)
TypeScript types complete
Exports added to public API
Error handling implemented
Safe decoding support
📦 Files Changed
7 files changed
2,779 insertions
0 deletions
New Files
decoders.ts
decoders.test.ts
DECODERS.md
decoder-usage.ts
Modified Files
index.ts
README.md
🚦 Ready for Review
This PR is ready for review and testing. All changes are backward compatible and require no modifications to existing code. The decoders are opt-in and can be adopted gradually.

🔮 Future Enhancements
Potential improvements for future PRs:

Schema validation from contract specs
Auto-generate decoders from contract metadata
Performance optimizations for large datasets
Decoder composition helpers
Custom error formatters
closes #221

rahimatonize and others added 2 commits April 27, 2026 08:45
- Implement comprehensive decoder library with 30+ decoders
- Add primitive decoders (string, number, bigint, boolean, bytes, address)
- Add composite decoders (array, tuple, struct, map, option)
- Add Soroban-specific decoders (u32, u64, u128, i32, i64, i128, BytesN)
- Add utility decoders (transform, validate, default, oneOf, enum, literal)
- Add pre-built contract decoders (Event, TicketTier, BuyerPurchase, TBA Token)
- Include safe decoding with result types
- Add comprehensive test suite with 50+ test cases
- Include detailed documentation and 15 usage examples

Resolves crowdpass-live#221
@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented Apr 27, 2026

@rahimatonize Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@manoahLinks manoahLinks merged commit a130b03 into crowdpass-live:main Apr 27, 2026
0 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

soroban-client: Provide typed decoder utilities for contract return values

2 participants