This implementation adds a batch royalty query function to the NFT smart contract, allowing frontends to fetch royalty data for multiple tokens in a single RPC call, significantly improving performance and reducing costs.
Location: contracts/nft-royalty/src/lib.rs
#[contracttype]
#[derive(Clone)]
pub struct BatchRoyaltyInfo {
pub token_id: u128,
pub recipient: Address,
pub fee_numerator: u32,
pub fee_denominator: u32,
}pub fn batch_royalty_info(env: Env, token_ids: Vec<u128>) -> Vec<BatchRoyaltyInfo>Features:
- Pure view function (no state changes, no access control)
- Returns royalty data for multiple tokens in one call
- Order-preserving: output[i] corresponds to input[i]
- Graceful degradation: non-existent tokens return zero values
- No on-chain batch size limit (RPC limits apply)
- Iterates over input token IDs
- Queries existing storage for each token
- Returns actual data for existing tokens
- Returns zero-value struct for non-existent tokens
- Never reverts on missing tokens
- Delegates to existing storage lookup logic
Location: src/nft/batch-royalty.service.ts
Features:
- Queries the smart contract's
batch_royalty_infofunction - Caches results in Redis for 5 minutes
- Enforces max batch size of 100 tokens
- Converts basis points to human-readable percentages
- Validates input token IDs
- Handles RPC errors gracefully
Response Format:
{
tokenId: "1",
recipient: "GABC...",
feeNumerator: 500,
feeDenominator: 10000,
royaltyPercentage: "5.00%"
}Endpoint: POST /nft/batch-royalty
Location: src/nft/batch-royalty.controller.ts
- Public endpoint (no authentication required)
- Accepts array of token IDs
- Returns array of royalty info in same order
- Max batch size: 100 tokens
- Cached for 5 minutes
Request:
{
"tokenIds": [1, 2, 3, 4, 5]
}Response:
[
{
"tokenId": "1",
"recipient": "GABC...",
"feeNumerator": 500,
"feeDenominator": 10000,
"royaltyPercentage": "5.00%"
},
...
]- Updated
contracts/nft-royalty/src/lib.rs- AddedBatchRoyaltyInfostruct andbatch_royalty_infofunction contracts/nft-royalty/BATCH_ROYALTY_QUERY.md- Comprehensive documentation
src/nft/batch-royalty.service.ts- Service for batch queriessrc/nft/batch-royalty.service.spec.ts- Unit testssrc/nft/batch-royalty.controller.ts- API controller
BATCH_ROYALTY_IMPLEMENTATION.md- This summary- Updated
contracts/nft-royalty/README.md- Added batch query info
src/nft/nft.module.ts- Added new service and controllercontracts/nft-royalty/src/lib.rs- Added batch query function and tests
All requirements met:
- Function Signature:
batch_royalty_info(token_ids: Vec<u128>) -> Vec<BatchRoyaltyInfo> - Pure View: No state mutations, no access control, callable by anyone
- Return Type:
BatchRoyaltyInfowith token_id, recipient, fee_numerator, fee_denominator - Reuses Existing Type: Uses existing
RoyaltyInfointernally - Implementation Logic: Iterates input, delegates to existing lookup, collects results
- Order Preservation: Output array matches input array order exactly
- Same Length: Output length equals input length
- Edge Case - Non-existent: Returns zero-value struct (no revert)
- Edge Case - Empty: Returns empty array immediately
- Edge Case - No Limit: No on-chain batch size limit
- No State Changes: Pure view function
- No Duplication: Delegates to existing storage lookup
- Documentation: NatSpec comments and comprehensive guides
- Warning: Documented RPC timeout risk for large batches
All tests passing:
test_batch_royalty_info_multiple_tokens- Verifies correct data for 3 tokens with different royaltiestest_batch_royalty_info_with_nonexistent_tokens- Tests mixed existing/non-existing tokenstest_batch_royalty_info_empty_input- Verifies empty array returns empty arraytest_batch_royalty_info_order_preservation- Confirms output order matches input ordertest_batch_royalty_info_single_token- Tests single-token batch
- Input validation tests
- Empty array handling
- Batch size limit enforcement
- Cache clearing functionality
// Fetch royalty data for multiple tokens
const response = await fetch('http://localhost:3000/nft/batch-royalty', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tokenIds: [1, 2, 3, 4, 5]
})
});
const royalties = await response.json();
// Display royalty info
royalties.forEach(info => {
if (info.feeNumerator > 0) {
console.log(`Token ${info.tokenId}: ${info.royaltyPercentage} to ${info.recipient}`);
} else {
console.log(`Token ${info.tokenId}: Not found or no royalty`);
}
});curl -X POST http://localhost:3000/nft/batch-royalty \
-H "Content-Type: application/json" \
-d '{"tokenIds": [1, 2, 3, 4, 5]}'let token_ids = vec![&env, 1, 2, 3, 4, 5];
let royalties = client.batch_royalty_info(&token_ids);
for i in 0..royalties.len() {
let info = royalties.get(i).unwrap();
// Process info.token_id, info.recipient, info.fee_numerator, etc.
}// 5 separate RPC calls
const royalties = [];
for (const tokenId of [1, 2, 3, 4, 5]) {
const response = await fetch(`/nft/royalty/${tokenId}`);
royalties.push(await response.json());
}
// Total time: ~5 seconds (1 second per call)// 1 RPC call
const response = await fetch('/nft/batch-royalty', {
method: 'POST',
body: JSON.stringify({ tokenIds: [1, 2, 3, 4, 5] })
});
const royalties = await response.json();
// Total time: ~1 secondImprovements:
- 80% reduction in RPC calls
- 80% reduction in response time
- 80% reduction in RPC costs
- Better cache utilization
- Improved user experience
┌─────────────────────────────────────────────────────────────┐
│ Stellar Blockchain │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ NFT Royalty Smart Contract (Soroban) │ │
│ │ │ │
│ │ Function: │ │
│ │ batch_royalty_info(token_ids: Vec<u128>) │ │
│ │ -> Vec<BatchRoyaltyInfo> │ │
│ │ │ │
│ │ For each token_id: │ │
│ │ - Query storage for royalty info │ │
│ │ - Return actual data if exists │ │
│ │ - Return zero values if not exists │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↑
│ Single RPC call
│
┌─────────────────────────────────────────────────────────────┐
│ Backend (NestJS) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ BatchRoyaltyService │ │
│ │ │ │
│ │ - Validates input (max 100 tokens) │ │
│ │ - Queries batch_royalty_info() │ │
│ │ - Caches in Redis (5 min TTL) │ │
│ │ - Converts to human-readable format │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ BatchRoyaltyController │ │
│ │ │ │
│ │ POST /nft/batch-royalty (public) │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↑
│ HTTP POST
│
┌──────────┐
│ Frontend │
└──────────┘
cd contracts/nft-royalty
# Build
cargo build --target wasm32-unknown-unknown --release
# Deploy to testnet
./scripts/deploy.sh testnet
# Update .env with contract ID
echo "SOROBAN_NFT_CONTRACT_ID=<your_contract_id>" >> ../../.envnpm install
npm run start:devcurl -X POST http://localhost:3000/nft/batch-royalty \
-H "Content-Type: application/json" \
-d '{"tokenIds": [1, 2, 3]}'| Use Case | Recommended Batch Size | Reason |
|---|---|---|
| Real-time UI | 10-20 tokens | Fast response, good UX |
| Gallery view | 20-50 tokens | Balance speed/data |
| Background sync | 50-100 tokens | Maximize efficiency |
| Bulk operations | 100 tokens (max) | API limit |
- Frontend: Cache results in memory/localStorage
- Backend: Redis cache (5 minutes)
- Invalidation: Clear cache after minting/updating royalties
try {
const response = await fetch('/nft/batch-royalty', {
method: 'POST',
body: JSON.stringify({ tokenIds })
});
if (!response.ok) {
if (response.status === 400) {
// Invalid input or batch too large
console.error('Invalid request');
} else if (response.status === 500) {
// RPC error or contract error
console.error('Server error');
}
}
const royalties = await response.json();
// Process royalties
} catch (error) {
// Network error
console.error('Network error:', error);
}- No Access Control: Function is public by design for transparency
- No State Changes: Pure view function cannot modify storage
- DoS Protection: Backend enforces max batch size (100 tokens)
- Input Validation: Token IDs validated as non-negative integers
- Rate Limiting: Standard API rate limits apply
- Cache Poisoning: Cache keys include all token IDs to prevent poisoning
- Average batch size
- Response time by batch size
- Cache hit rate
- RPC call frequency
- Error rate by type
// Service logs
this.logger.log(`Batch royalty query for ${tokenIds.length} tokens`);
this.logger.debug(`Cache hit for batch: ${tokenIds.join(',')}`);
this.logger.error(`RPC error: ${error.message}`);Potential additions (not in current scope):
- Pagination: Support for very large batches with cursor-based pagination
- Filtering: Query only tokens with royalties above a threshold
- Sorting: Return results sorted by royalty percentage or token ID
- Aggregation: Include summary statistics (total, average, min, max)
- WebSocket: Real-time updates when royalties change
- GraphQL: Alternative query interface with flexible field selection
For questions or issues:
- Smart Contract: See
contracts/nft-royalty/BATCH_ROYALTY_QUERY.md - Backend: See
src/nft/batch-royalty.service.ts - API: See
src/nft/batch-royalty.controller.ts - Tests: Run
cargo test(contract) ornpm test(backend)