From d2d0a52c5ab6bcbb535f3b44d574dc73cb7cb59a Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Wed, 9 Apr 2025 20:27:21 -0700 Subject: [PATCH 01/12] docs: new Q&A questions and answers section --- .../composite-keys-for-complex-queries.md | 100 ++++++++ .../concurrent-operations-in-chaincode.md | 230 +++++++++++++++++ .../data-relationships-in-chaincode.md | 219 ++++++++++++++++ .../emit-and-handle-events-in-chaincode.md | 130 ++++++++++ .../errors-and-exceptions-in-chaincode.md | 237 ++++++++++++++++++ .../handle-data-deletion-in-chaincode.md | 139 ++++++++++ .../handle-data-migrations-in-chaincode.md | 128 ++++++++++ .../handle-data-validation-in-chaincode.md | 117 +++++++++ .../questions/handle-timeouts-in-chaincode.md | 80 ++++++ docs/questions/implement-batch-operations.md | 194 ++++++++++++++ .../implement-data-versioning-in-chaincode.md | 134 ++++++++++ ...mplement-integration-tests-in-chaincode.md | 188 ++++++++++++++ ...mplement-pagination-for-large-data-sets.md | 97 +++++++ .../implement-rate-limiting-in-chaincode.md | 30 +++ ...implement-retry-mechanisms-in-chaincode.md | 43 ++++ .../implement-rich-queries-using-couchdb.md | 82 ++++++ docs/questions/logging-best-practices.md | 221 ++++++++++++++++ ...ptimizie-query-performance-in-chaincode.md | 198 +++++++++++++++ .../store-and-query-data-in-chaincode.md | 81 ++++++ docs/questions/unit-tests-for-chaincode.md | 166 ++++++++++++ package.json | 2 +- tools/scripts/questions-to-jsonl.ts | 117 +++++++++ 22 files changed, 2932 insertions(+), 1 deletion(-) create mode 100644 docs/questions/composite-keys-for-complex-queries.md create mode 100644 docs/questions/concurrent-operations-in-chaincode.md create mode 100644 docs/questions/data-relationships-in-chaincode.md create mode 100644 docs/questions/emit-and-handle-events-in-chaincode.md create mode 100644 docs/questions/errors-and-exceptions-in-chaincode.md create mode 100644 docs/questions/handle-data-deletion-in-chaincode.md create mode 100644 docs/questions/handle-data-migrations-in-chaincode.md create mode 100644 docs/questions/handle-data-validation-in-chaincode.md create mode 100644 docs/questions/handle-timeouts-in-chaincode.md create mode 100644 docs/questions/implement-batch-operations.md create mode 100644 docs/questions/implement-data-versioning-in-chaincode.md create mode 100644 docs/questions/implement-integration-tests-in-chaincode.md create mode 100644 docs/questions/implement-pagination-for-large-data-sets.md create mode 100644 docs/questions/implement-rate-limiting-in-chaincode.md create mode 100644 docs/questions/implement-retry-mechanisms-in-chaincode.md create mode 100644 docs/questions/implement-rich-queries-using-couchdb.md create mode 100644 docs/questions/logging-best-practices.md create mode 100644 docs/questions/optimizie-query-performance-in-chaincode.md create mode 100644 docs/questions/store-and-query-data-in-chaincode.md create mode 100644 docs/questions/unit-tests-for-chaincode.md create mode 100644 tools/scripts/questions-to-jsonl.ts diff --git a/docs/questions/composite-keys-for-complex-queries.md b/docs/questions/composite-keys-for-complex-queries.md new file mode 100644 index 0000000000..22e7a6a273 --- /dev/null +++ b/docs/questions/composite-keys-for-complex-queries.md @@ -0,0 +1,100 @@ +### Question + + +How can I use composite keys for complex queries in chaincode? + + +### Answer + + +Composite keys in GalaChain enable efficient querying by combining multiple fields into a single key. Here's how to use them effectively: + +1. Design your composite key structure: +```typescript +export class TokenBalance extends ChainObject { + @Exclude() + public static readonly INDEX_KEY = 'TOKEN_BALANCE'; + + @ChainKey({ position: 0 }) + @IsUserAlias() + public readonly owner: UserAlias; // Primary query field + + @ChainKey({ position: 1 }) + @IsString() + public readonly tokenClass: string; // Secondary query field + + @ChainKey({ position: 2 }) + @IsString() + public readonly tokenInstance: string; // Tertiary query field + + @Min(0) + public readonly quantity: BigNumber; +} +``` + +2. Query using partial composite keys for flexible filtering: +```typescript +// Query all balances for an owner +async function getOwnerBalances( + ctx: GalaChainContext, + owner: UserAlias +): Promise { + return getObjectsByPartialCompositeKey( + ctx, + TokenBalance.INDEX_KEY, + [owner] // Just the first key part + ); +} + +// Query balances for specific token class +async function getOwnerTokenClassBalances( + ctx: GalaChainContext, + owner: UserAlias, + tokenClass: string +): Promise { + return getObjectsByPartialCompositeKey( + ctx, + TokenBalance.INDEX_KEY, + [owner, tokenClass] // First two key parts + ); +} + +// Query specific token instance balance +async function getSpecificBalance( + ctx: GalaChainContext, + owner: UserAlias, + tokenClass: string, + tokenInstance: string +): Promise { + return getObjectsByPartialCompositeKey( + ctx, + TokenBalance.INDEX_KEY, + [owner, tokenClass, tokenInstance] // All key parts + ); +} +``` + +3. Create composite keys manually when needed: +```typescript +// Create a complete composite key +const completeKey = ChainObject.getCompositeKeyFromParts( + TokenBalance.INDEX_KEY, + [owner, tokenClass, tokenInstance] +); + +// Use for direct lookups +const balance = await getObjectByKey(ctx, TokenBalance, completeKey); +``` + +Key concepts: +- Order matters: Chain keys must be queried in order (can't skip positions) +- Progressive filtering: Add more key parts for more specific queries +- Performance: Composite keys enable fast range queries and prefix scans +- Uniqueness: Complete composite keys must be unique within a class + +Best practices: +- Order chain keys from most to least frequently queried +- Use meaningful key parts that support your query patterns +- Keep key parts short to minimize storage overhead +- Consider index size impact when designing composite keys +- Document the composite key structure in class comments \ No newline at end of file diff --git a/docs/questions/concurrent-operations-in-chaincode.md b/docs/questions/concurrent-operations-in-chaincode.md new file mode 100644 index 0000000000..9c37426c60 --- /dev/null +++ b/docs/questions/concurrent-operations-in-chaincode.md @@ -0,0 +1,230 @@ +### Question + + +How do I implement concurrent operations in chaincode? + + +### Answer + + +GalaChain provides built-in mechanisms for handling concurrent operations safely. Here's how to implement and manage concurrency: + +1. Version-Based Concurrency: +```typescript +export class VersionedAsset extends ChainObject { + @ChainKey({ position: 0 }) + public readonly assetId: string; + + public readonly version: number; + public readonly data: any; + + public nextVersion(newData: any): VersionedAsset { + return new VersionedAsset({ + ...this, + version: this.version + 1, + data: newData + }); + } +} + +@Submit() +async function updateAsset( + ctx: GalaChainContext, + params: { + assetId: string; + expectedVersion: number; + newData: any; + } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + VersionedAsset.INDEX_KEY, + [params.assetId] + ); + + const asset = await getObjectByKey(ctx, VersionedAsset, key); + if (!asset) { + throw new Error('Asset not found'); + } + + // Version check for optimistic locking + if (asset.version !== params.expectedVersion) { + throw new Error('Version mismatch - asset was modified'); + } + + // Update with new version + const updated = asset.nextVersion(params.newData); + await putChainObject(ctx, updated); +} +``` + +2. Atomic Operations: +```typescript +@Submit() +async function transferBetweenAccounts( + ctx: GalaChainContext, + params: { + fromId: string; + toId: string; + amount: number; + } +): Promise { + // All operations in this transaction are atomic + const fromBalance = await getBalance(ctx, params.fromId); + if (!fromBalance || fromBalance.amount < params.amount) { + throw new Error('Insufficient balance'); + } + + const toBalance = await getBalance(ctx, params.toId); + if (!toBalance) { + throw new Error('Recipient account not found'); + } + + fromBalance.amount -= params.amount; + toBalance.amount += params.amount; + + // Both operations will succeed or fail together, because of GalaChainStubCache handling + await putChainObject(ctx, fromBalance); + await putChainObject(ctx, toBalance); +} +``` + +3. Handling Race Conditions: +```typescript +export class LockableAsset extends ChainObject { + @ChainKey({ position: 0 }) + public readonly assetId: string; + + public readonly lockedBy?: string; + public readonly lockExpiry?: number; + public readonly data: any; + + public isLocked(): boolean { + return this.lockExpiry != null && + this.lockExpiry > Date.now(); + } + + public lock(userId: string): LockableAsset { + return new LockableAsset({ + ...this, + lockedBy: userId, + lockExpiry: Date.now() + 60000 // 1 minute lock + }); + } + + public unlock(): LockableAsset { + return new LockableAsset({ + ...this, + lockedBy: undefined, + lockExpiry: undefined + }); + } +} + +@Submit() +async function modifyWithLock( + ctx: GalaChainContext, + params: { + assetId: string; + userId: string; + modifications: any; + } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + LockableAsset.INDEX_KEY, + [params.assetId] + ); + + const asset = await getObjectByKey(ctx, LockableAsset, key); + if (!asset) { + throw new Error('Asset not found'); + } + + if (asset.isLocked() && asset.lockedBy !== params.userId) { + throw new Error('Asset is locked by another user'); + } + + // Acquire lock + const locked = asset.lock(params.userId); + await putChainObject(ctx, locked); + + try { + // Perform modifications + const modified = new LockableAsset({ + ...locked, + data: { + ...locked.data, + ...params.modifications + } + }); + + // Release lock and save changes + const unlocked = modified.unlock(); + await putChainObject(ctx, unlocked); + } catch (error) { + // Ensure lock is released on error + const unlocked = asset.unlock(); + await putChainObject(ctx, unlocked); + throw error; + } +} +``` + +4. Batch Processing with Concurrency: +```typescript +@Submit() +async function batchProcess( + ctx: GalaChainContext, + params: { + items: Array<{ + id: string; + operation: string; + data: any; + }>; + } +): Promise<{ + succeeded: string[]; + failed: Array<{ id: string; error: string }>; +}> { + const results = { + succeeded: [] as string[], + failed: [] as Array<{ id: string; error: string }> + }; + + // Process items sequentially to maintain consistency + for (const item of params.items) { + try { + await processItem(ctx, item); + results.succeeded.push(item.id); + } catch (error) { + results.failed.push({ + id: item.id, + error: error.message + }); + // Continue processing other items + } + } + + return results; +} +``` + +Best Practices: +- Use version-based concurrency +- Leverage atomic transactions +- Implement proper locking +- Handle race conditions +- Process batches carefully + +Key Points: +- All chaincode operations are atomic +- Use optimistic locking +- Implement proper error handling +- Consider transaction boundaries +- Monitor concurrent access + +Concurrency Tips: +- Keep transactions short +- Handle timeouts appropriately +- Implement retry mechanisms +- Log concurrent operations +- Test concurrent scenarios \ No newline at end of file diff --git a/docs/questions/data-relationships-in-chaincode.md b/docs/questions/data-relationships-in-chaincode.md new file mode 100644 index 0000000000..9bfc0cff26 --- /dev/null +++ b/docs/questions/data-relationships-in-chaincode.md @@ -0,0 +1,219 @@ +### Question + + +What's the best way to handle data relationships in chaincode? + + +### Answer + + +GalaChain provides several patterns for managing relationships between data objects. Here's how to implement different types of relationships: + +1. Reference by Key: +```typescript +export class GameItem extends ChainObject { + @ChainKey({ position: 0 }) + public readonly itemId: string; + + // Reference to owner + @IsUserAlias() + public readonly ownerId: string; + + // Reference to item class + public readonly itemClassId: string; + + // Helper method to get owner + public async getOwner( + ctx: GalaChainContext + ): Promise { + const key = ChainObject.getCompositeKeyFromParts( + UserProfile.INDEX_KEY, + [this.ownerId] + ); + return getObjectByKey(ctx, UserProfile, key); + } + + // Helper method to get item class + public async getItemClass( + ctx: GalaChainContext + ): Promise { + const key = ChainObject.getCompositeKeyFromParts( + ItemClass.INDEX_KEY, + [this.itemClassId] + ); + return getObjectByKey(ctx, ItemClass, key); + } +} +``` + +2. Shared Key Structure: +```typescript +// Token class definition +export class TokenClass extends ChainObject { + @ChainKey({ position: 0 }) + public readonly collection: string; + + @ChainKey({ position: 1 }) + public readonly category: string; + + @ChainKey({ position: 2 }) + public readonly type: string; + + @ChainKey({ position: 3 }) + public readonly additionalKey: string; + + // Class-specific properties + public readonly maxSupply: number; + public readonly metadata: TokenMetadata; +} + +// Token balance sharing same key structure +export class TokenBalance extends ChainObject { + @ChainKey({ position: 0 }) + public readonly collection: string; + + @ChainKey({ position: 1 }) + public readonly category: string; + + @ChainKey({ position: 2 }) + public readonly type: string; + + @ChainKey({ position: 3 }) + public readonly additionalKey: string; + + @ChainKey({ position: 4 }) + @IsUserAlias() + public readonly owner: string; + + // Balance-specific properties + public readonly balance: number; +} + +// Query method using shared key structure +async function getTokenBalancesByClass( + ctx: GalaChainContext, + tokenClass: TokenClass +): Promise { + return getObjectsByPartialCompositeKey( + ctx, + TokenBalance.INDEX_KEY, + [ + tokenClass.collection, + tokenClass.category, + tokenClass.type, + tokenClass.additionalKey + ] + ); +} +``` + +3. One-to-Many Relationships: +```typescript +export class Inventory extends ChainObject { + @ChainKey({ position: 0 }) + @IsUserAlias() + public readonly userId: string; + + public readonly items: string[]; // Array of item IDs + + // Helper method to get all items + public async getItems( + ctx: GalaChainContext + ): Promise { + const items: GameItem[] = []; + + for (const itemId of this.items) { + const key = ChainObject.getCompositeKeyFromParts( + GameItem.INDEX_KEY, + [itemId] + ); + const item = await getObjectByKey(ctx, GameItem, key); + if (item) { + items.push(item); + } + } + + return items; + } +} +``` + +4. Many-to-Many Relationships: +```typescript +export class TeamMembership extends ChainObject { + @ChainKey({ position: 0 }) + public readonly teamId: string; + + @ChainKey({ position: 1 }) + @IsUserAlias() + public readonly userId: string; + + public readonly role: string; +} + +// Query team members +async function getTeamMembers( + ctx: GalaChainContext, + teamId: string +): Promise { + return getObjectsByPartialCompositeKey( + ctx, + TeamMembership.INDEX_KEY, + [teamId] + ); +} + +// Query user's teams +async function getUserTeams( + ctx: GalaChainContext, + userId: string +): Promise { + const memberships = await getObjectsByPartialCompositeKey( + ctx, + TeamMembership.INDEX_KEY, + [] + ); + + const userMemberships = memberships.filter( + m => m.userId === userId + ); + + const teams: Team[] = []; + for (const membership of userMemberships) { + const team = await getObjectByKey( + ctx, + Team, + ChainObject.getCompositeKeyFromParts( + Team.INDEX_KEY, + [membership.teamId] + ) + ); + if (team) { + teams.push(team); + } + } + + return teams; +} +``` + +Best Practices: +- Use consistent key structures for related objects +- Implement helper methods for relationship navigation +- Consider query performance when designing relationships +- Validate references before saving +- Document relationship patterns + +Key Points: +- Objects can reference others via key properties +- Shared key structures enable efficient querying +- Helper methods simplify relationship traversal +- Consider denormalization for performance +- Maintain referential integrity + +Design Considerations: +- Balance normalization vs query performance +- Plan for relationship changes +- Consider index impact +- Handle missing references +- Document relationship patterns \ No newline at end of file diff --git a/docs/questions/emit-and-handle-events-in-chaincode.md b/docs/questions/emit-and-handle-events-in-chaincode.md new file mode 100644 index 0000000000..6280fc671f --- /dev/null +++ b/docs/questions/emit-and-handle-events-in-chaincode.md @@ -0,0 +1,130 @@ +### Question + + +What's the best way to emit and handle events in chaincode? + + +### Answer + + +While Hyperledger Fabric includes event emission capabilities, GalaChain currently does not implement this feature. Here's what you need to know: + +1. Current Implementation Status: +```typescript +// In GalaChainStub.ts +export class GalaChainStub implements ChaincodeStub { + // ... other methods + + setEvent(name: string, payload: Uint8Array): void { + throw new NotImplementedError("setEvent is not supported"); + } + + // ... other methods +} +``` + +2. Alternative Approaches: +```typescript +// Instead of events, use direct state updates +@Submit() +async function transferToken( + ctx: GalaChainContext, + params: { + fromId: string; + toId: string; + tokenId: string; + } +): Promise { + // Perform transfer + const result = await processTransfer(ctx, params); + + // Return result directly + return { + status: 'success', + tokenId: params.tokenId, + fromId: params.fromId, + toId: params.toId, + timestamp: Date.now() + }; +} + +// Query state changes directly +@Query() +async function getRecentTransfers( + ctx: GalaChainContext, + params: { userId: string } +): Promise { + return getObjectsByPartialCompositeKey( + ctx, + TransferResult.INDEX_KEY, + [params.userId] + ); +} +``` + +3. Operations API Integration: +```typescript +// Define operation result +export class OperationResult extends ChainObject { + @ChainKey({ position: 0 }) + public readonly operationId: string; + + public readonly status: 'success' | 'failed'; + public readonly timestamp: number; + public readonly details: any; +} + +// Store operation result +@Submit() +async function completeOperation( + ctx: GalaChainContext, + params: { + operationId: string; + status: 'success' | 'failed'; + details: any; + } +): Promise { + const result = new OperationResult({ + operationId: params.operationId, + status: params.status, + timestamp: Date.now(), + details: params.details + }); + + await putChainObject(ctx, result); +} + +// Query operation status +@Query() +async function getOperationStatus( + ctx: GalaChainContext, + params: { operationId: string } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + OperationResult.INDEX_KEY, + [params.operationId] + ); + return getObjectByKey(ctx, OperationResult, key); +} +``` + +Best Practices: +- Use direct state queries instead of events +- Leverage the Operations API for status tracking +- Store operation results in chain state +- Implement polling mechanisms when needed +- Consider transaction finality + +Key Points: +- Event emission is not currently supported +- Fast transaction settlement reduces need for events +- Operations API provides status tracking +- State queries offer reliable data access +- Design for synchronous operations + +Design Considerations: +- Plan for synchronous workflows +- Store relevant state changes +- Implement proper error handling +- Consider query performance +- Document state transitions \ No newline at end of file diff --git a/docs/questions/errors-and-exceptions-in-chaincode.md b/docs/questions/errors-and-exceptions-in-chaincode.md new file mode 100644 index 0000000000..a2d0e8e0ab --- /dev/null +++ b/docs/questions/errors-and-exceptions-in-chaincode.md @@ -0,0 +1,237 @@ +### Question + + +How do I handle errors and exceptions in chaincode? + + +### Answer + + +GalaChain provides a robust error handling system centered around the `DefaultError` class. Here's how to implement proper error handling: + +1. Custom Error Classes: +```typescript +import { DefaultError } from '@gala-chain/api'; + +export class InsufficientBalanceError extends DefaultError { + constructor( + public readonly userId: string, + public readonly required: number, + public readonly available: number + ) { + super( + 'INSUFFICIENT_BALANCE', + `User ${userId} has insufficient balance. Required: ${required}, Available: ${available}` + ); + } + + public static is(error: Error): error is InsufficientBalanceError { + return error instanceof InsufficientBalanceError; + } +} + +export class TokenNotFoundError extends DefaultError { + constructor( + public readonly tokenId: string + ) { + super( + 'TOKEN_NOT_FOUND', + `Token ${tokenId} not found` + ); + } + + public static is(error: Error): error is TokenNotFoundError { + return error instanceof TokenNotFoundError; + } +} +``` + +2. Error Handling in Chaincode: +```typescript +@Submit() +async function transferTokens( + ctx: GalaChainContext, + params: { + fromId: string; + toId: string; + amount: number; + } +): Promise { + try { + // Get source balance + const fromBalance = await getBalance(ctx, params.fromId); + if (!fromBalance) { + throw new TokenNotFoundError(params.fromId); + } + + // Check sufficient balance + if (fromBalance.amount < params.amount) { + throw new InsufficientBalanceError( + params.fromId, + params.amount, + fromBalance.amount + ); + } + + // Process transfer + await processTransfer(ctx, params); + } catch (error) { + // Handle specific errors + if (InsufficientBalanceError.is(error)) { + // Log detailed balance info + ctx.logger.error( + `Balance check failed: ${error.required} > ${error.available}` + ); + throw error; + } + + if (TokenNotFoundError.is(error)) { + ctx.logger.error(`Token lookup failed: ${error.tokenId}`); + throw error; + } + + // Handle unexpected errors + ctx.logger.error('Unexpected error during transfer:', error); + throw new DefaultError( + 'TRANSFER_FAILED', + 'Transfer failed due to an unexpected error' + ); + } +} +``` + +3. Validation Errors: +```typescript +export class ValidationError extends DefaultError { + constructor( + public readonly errors: string[] + ) { + super( + 'VALIDATION_FAILED', + `Validation failed: ${errors.join(', ')}` + ); + } +} + +@Submit() +async function createAsset( + ctx: GalaChainContext, + params: GameAssetParams +): Promise { + try { + const asset = new GameAsset(params); + await validateOrReject(asset); + await putChainObject(ctx, asset); + return asset; + } catch (error) { + if (error instanceof Array) { + // Handle class-validator errors + const validationErrors = error.map(e => + Object.values(e.constraints).join(', ') + ); + throw new ValidationError(validationErrors); + } + throw error; + } +} +``` + +4. Error Recovery: +```typescript +export class RetryableError extends DefaultError { + constructor( + message: string, + public readonly retryAfter: number = 5000 + ) { + super('RETRYABLE_ERROR', message); + } +} + +async function retryOperation({ + operation, + maxAttempts = 3, + delayMs = 1000 +}: { + operation: () => Promise; + maxAttempts?: number; + delayMs?: number; +}): Promise { + let lastError: Error; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await operation(); + } catch (error) { + lastError = error; + + if (error instanceof RetryableError) { + if (attempt < maxAttempts) { + await new Promise(resolve => + setTimeout(resolve, error.retryAfter || delayMs) + ); + continue; + } + } + break; + } + } + + throw lastError; +} +``` + +5. Error Aggregation: +```typescript +export class BatchError extends DefaultError { + constructor( + public readonly errors: Error[] + ) { + super( + 'BATCH_OPERATION_FAILED', + `Multiple errors occurred: ${errors.map(e => e.message).join('; ')}` + ); + } +} + +async function batchOperation( + operations: (() => Promise)[] +): Promise { + const results: T[] = []; + const errors: Error[] = []; + + for (const operation of operations) { + try { + results.push(await operation()); + } catch (error) { + errors.push(error); + } + } + + if (errors.length > 0) { + throw new BatchError(errors); + } + + return results; +} +``` + +Best Practices: +- Extend DefaultError for custom errors +- Include relevant error context +- Implement type guards (is methods) +- Use specific error types +- Handle errors at appropriate levels + +Key Points: +- Structured error handling +- Type-safe error checking +- Detailed error messages +- Error recovery patterns +- Proper error propagation + +Error Design Tips: +- Keep error hierarchies shallow +- Include actionable information +- Consider error recovery +- Log errors appropriately +- Document error conditions \ No newline at end of file diff --git a/docs/questions/handle-data-deletion-in-chaincode.md b/docs/questions/handle-data-deletion-in-chaincode.md new file mode 100644 index 0000000000..b68ac6d0b8 --- /dev/null +++ b/docs/questions/handle-data-deletion-in-chaincode.md @@ -0,0 +1,139 @@ +### Question + + +What's the proper way to handle data deletion in chaincode? + + +### Answer + + +In GalaChain, data deletion should be handled carefully since blockchain is an append-only ledger. Here's how to properly handle data deletion: + +1. Using `deleteChainObject`: +```typescript +@Submit() +async function deleteGameItem( + ctx: GalaChainContext, + params: { itemId: string } +): Promise { + // First, verify the item exists + const key = ChainObject.getCompositeKeyFromParts( + GameItem.INDEX_KEY, + [params.itemId] + ); + const item = await getObjectByKey(ctx, GameItem, key); + + if (!item) { + throw new Error('Item not found'); + } + + // Delete the item + await deleteChainObject(ctx, item); +} +``` + +2. Implementing Soft Deletion: +```typescript +export class GameItem extends ChainObject { + @ChainKey({ position: 0 }) + public readonly itemId: string; + + public readonly name: string; + public readonly deleted: boolean; + + // Helper method for soft deletion + public markDeleted(): GameItem { + return new GameItem({ + ...this, + deleted: true + }); + } +} + +@Submit() +async function softDeleteItem( + ctx: GalaChainContext, + params: { itemId: string } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + GameItem.INDEX_KEY, + [params.itemId] + ); + const item = await getObjectByKey(ctx, GameItem, key); + + if (!item) { + throw new Error('Item not found'); + } + + // Mark as deleted and update + const deletedItem = item.markDeleted(); + await putChainObject(ctx, deletedItem); +} + +// Query that excludes deleted items +async function getActiveItems( + ctx: GalaChainContext +): Promise { + const items = await getObjectsByPartialCompositeKey( + ctx, + GameItem.INDEX_KEY, + [] + ); + return items.filter(item => !item.deleted); +} +``` + +3. Handling Related Data: +```typescript +@Submit() +async function deleteItemWithRelations( + ctx: GalaChainContext, + params: { itemId: string } +): Promise { + // Delete main item + const item = await getObjectByKey( + ctx, + GameItem, + ChainObject.getCompositeKeyFromParts(GameItem.INDEX_KEY, [params.itemId]) + ); + + if (!item) { + throw new Error('Item not found'); + } + + // Delete related metadata + const metadata = await getObjectByKey( + ctx, + ItemMetadata, + ChainObject.getCompositeKeyFromParts(ItemMetadata.INDEX_KEY, [params.itemId]) + ); + + if (metadata) { + await deleteChainObject(ctx, metadata); + } + + // Delete the main item last + await deleteChainObject(ctx, item); +} +``` + +Best Practices: +- Consider soft deletion for data that might need to be referenced +- Always verify object existence before deletion +- Handle related data deletion in a consistent order +- Document deletion policies and procedures +- Implement access control for deletion operations + +Key Points: +- Use `deleteChainObject` for permanent removal +- Implement soft deletion when history is important +- Handle cascading deletes carefully +- Consider implementing recovery mechanisms +- Remember that blockchain history remains + +Security Considerations: +- Validate permissions before deletion +- Log deletion operations for audit +- Consider impact on related data +- Implement proper error handling +- Test deletion scenarios thoroughly \ No newline at end of file diff --git a/docs/questions/handle-data-migrations-in-chaincode.md b/docs/questions/handle-data-migrations-in-chaincode.md new file mode 100644 index 0000000000..29484b4113 --- /dev/null +++ b/docs/questions/handle-data-migrations-in-chaincode.md @@ -0,0 +1,128 @@ +### Question + + +How can I handle data migrations in chaincode? + + +### Answer + + +When modifying data structures in GalaChain chaincode, particularly classes that extend `ChainObject`, you have several strategies to handle existing on-chain data. Here's how to approach data migrations: + +1. Backward Compatible Changes: +```typescript +import { ChainObject, ChainKey, IsOptional } from '@gala-chain/api'; + +// Original version +export class GameItem extends ChainObject { + @ChainKey({ position: 0 }) + public readonly itemId: string; + + public readonly name: string; +} + +// Updated version with backward compatibility +export class GameItem extends ChainObject { + @ChainKey({ position: 0 }) + public readonly itemId: string; + + public readonly name: string; + + @IsOptional() // Makes the new field optional + public readonly rarity?: string; // Won't break existing records + + public getRarity(): string { + return this.rarity ?? 'common'; // Provide default for old records + } +} +``` + +2. Data Migration Method: +```typescript +@Submit() +async function migrateGameItems( + ctx: GalaChainContext, + params: { batchSize: number } +): Promise { + let bookmark = ''; + + do { + // Get a batch of items + const { results, metadata } = await getObjectsByPartialCompositeKeyWithPagination( + ctx, + GameItem.INDEX_KEY, + [], // Empty array to get all items + GameItem, + params.batchSize, + bookmark + ); + + // Update each item + for (const item of results) { + const updatedItem = new GameItem({ + ...item, + rarity: calculateRarityFromStats(item), // Set new field + }); + + await putChainObject(ctx, updatedItem); + } + + bookmark = metadata.bookmark; + } while (bookmark); +} +``` + +3. Schema Version Tracking: +```typescript +export class GameItem extends ChainObject { + @ChainKey({ position: 0 }) + public readonly itemId: string; + + public readonly schemaVersion: number = 2; // Track version + + public readonly name: string; + public readonly rarity: string; + + public static fromLegacy(old: any): GameItem { + if (!old.schemaVersion || old.schemaVersion === 1) { + return new GameItem({ + ...old, + rarity: 'common', // Default value for legacy data + schemaVersion: 2 + }); + } + return old as GameItem; + } +} + +// Use in queries +async function getItem(ctx: GalaChainContext, itemId: string): Promise { + const item = await getObjectByKey( + ctx, + GameItem, + ChainObject.getCompositeKeyFromParts(GameItem.INDEX_KEY, [itemId]) + ); + return GameItem.fromLegacy(item); +} +``` + +Best Practices: +- Always make new fields optional when possible +- Provide default values for backward compatibility +- Consider implementing migration methods for large-scale updates +- Test migrations thoroughly in development environment +- Document data model changes and migration procedures + +Key Points: +- Use `@IsOptional()` for new fields to maintain compatibility +- Implement migration methods for non-compatible changes +- Consider batching for large-scale migrations +- Track schema versions to handle multiple migrations +- Always validate migrated data before saving + +Migration Strategy Checklist: +1. Assess impact of data model changes +2. Choose appropriate migration strategy +3. Implement and test migration code +4. Plan migration execution timing +5. Monitor migration progress and validate results \ No newline at end of file diff --git a/docs/questions/handle-data-validation-in-chaincode.md b/docs/questions/handle-data-validation-in-chaincode.md new file mode 100644 index 0000000000..3b125bca1e --- /dev/null +++ b/docs/questions/handle-data-validation-in-chaincode.md @@ -0,0 +1,117 @@ +### Question + + +What's the best way to handle data validation in chaincode? + + +### Answer + + +GalaChain provides robust data validation through decorators and built-in validation hooks. Here's how to implement comprehensive validation: + +1. Use class-validator decorators for property validation: +```typescript +import { ChainObject, IsNotEmpty, IsUserAlias, Min, Max } from '@gala-chain/api'; + +export class TokenMint extends ChainObject { + @IsUserAlias() // Validates user alias format + @IsNotEmpty() // Ensures field is not empty + public readonly owner: UserAlias; + + @Min(0) // Minimum value check + @Max(1000000) // Maximum value check + public readonly quantity: BigNumber; + + @IsString() // Type validation + @Length(3, 50) // String length validation + public readonly tokenClass: string; + + @ValidateNested() // Validates nested objects + @Type(() => TokenMetadata) + public readonly metadata?: TokenMetadata; +} +``` + +2. Implement custom validators for complex rules: +```typescript +import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator'; + +@ValidatorConstraint({ name: 'customTokenRule', async: true }) +class TokenRuleValidator implements ValidatorConstraintInterface { + async validate(value: string, args: ValidationArguments) { + // Custom validation logic + return value.startsWith('GAME_') && value.length <= 32; + } + + defaultMessage(args: ValidationArguments) { + return 'Token class must start with GAME_ and be <= 32 chars'; + } +} + +export class GameToken extends ChainObject { + @Validate(TokenRuleValidator) + public readonly tokenClass: string; +} +``` + +3. Add validation in chaincode methods: +```typescript +@Submit() +async function mintToken( + ctx: GalaChainContext, + params: TokenMintParams +): Promise { + // Pre-validation checks + if (!params.owner) { + throw new ValidationError('Owner is required'); + } + + // Create and validate the mint object + const mint = plainToClass(TokenMint, { + ...params, + timestamp: Date.now() + }); + + // This will throw if validation fails + await validateOrReject(mint); + + // Additional business logic validation + const existingBalance = await getBalance(ctx, params.owner); + if (existingBalance.gt(MAX_ALLOWED)) { + throw new ValidationError('Would exceed maximum allowed balance'); + } + + // Proceed with mint if all validation passes + return putChainObject(ctx, mint); +} +``` + +4. Handle validation errors gracefully: +```typescript +try { + await validateOrReject(tokenMint); +} catch (errors) { + if (Array.isArray(errors)) { + // Class-validator errors + const messages = errors + .map(err => Object.values(err.constraints)) + .flat(); + throw new ValidationError(`Invalid token mint: ${messages.join(', ')}`); + } + throw errors; +} +``` + +Key validation principles: +- Validate at the model level using decorators +- Add custom validators for complex rules +- Perform business logic validation in methods +- Always validate before state changes +- Handle validation errors consistently + +Best practices: +- Use built-in validators when possible +- Keep validation rules close to data models +- Validate early in the request lifecycle +- Provide clear validation error messages +- Consider performance impact of async validators \ No newline at end of file diff --git a/docs/questions/handle-timeouts-in-chaincode.md b/docs/questions/handle-timeouts-in-chaincode.md new file mode 100644 index 0000000000..93fccb342e --- /dev/null +++ b/docs/questions/handle-timeouts-in-chaincode.md @@ -0,0 +1,80 @@ +### Question + + +What's the best way to handle timeouts in chaincode? + + +### Answer + + +Timeouts in chaincode can occur for several reasons. Here's how to handle them effectively: + +1. Common Timeout Causes: + - Large range queries without pagination + - Complex operations with many state reads/writes + - Network latency or peer resource constraints + - MVCC_READ_CONFLICT errors causing retries + +2. Implementation Example: +```typescript +class GameContract extends Contract { + @Submit() + async processItems( + ctx: GalaChainContext, + params: { items: string[] } + ): Promise { + const BATCH_SIZE = 20; + const batches = this.createBatches(params.items, BATCH_SIZE); + + for (const batch of batches) { + await this.processBatch(ctx, { items: batch }); + + ctx.logger.info('Batch processed', { + batchSize: batch.length, + remaining: params.items.length - batch.length + }); + } + } + + private createBatches(items: T[], size: number): T[][] { + const batches: T[][] = []; + for (let i = 0; i < items.length; i += size) { + batches.push(items.slice(i, i + size)); + } + return batches; + } + + private async processBatch( + ctx: GalaChainContext, + params: { items: string[] } + ): Promise { + // Process each item with proper error handling + for (const item of params.items) { + try { + await this.processItem(ctx, item); + } catch (error) { + ctx.logger.error('Item processing failed', { + item, + error: error.message + }); + throw error; + } + } + } +} +``` + +3. Best Practices: + - Use pagination for range queries + - Process large datasets in small batches + - Implement proper error handling and logging + - Monitor transaction execution times + - Consider network conditions and peer resources + +4. Configuration Tips: + - Adjust batch sizes based on data complexity + - Set appropriate timeouts in client applications + - Monitor and tune peer resource allocation + - Use proper indexes for frequently queried data + +Note: If you're experiencing frequent timeouts, review your chaincode's data model and query patterns. Consider implementing caching or denormalization strategies where appropriate. \ No newline at end of file diff --git a/docs/questions/implement-batch-operations.md b/docs/questions/implement-batch-operations.md new file mode 100644 index 0000000000..6e6d9362dd --- /dev/null +++ b/docs/questions/implement-batch-operations.md @@ -0,0 +1,194 @@ +### Question + + +How can I implement batch operations in chaincode? + + +### Answer + + +GalaChain provides several patterns for implementing batch operations efficiently. Here's how to handle different types of batch operations: + +1. Batch Creation: +```typescript +@Submit() +async function batchCreateAssets( + ctx: GalaChainContext, + params: { assets: GameAssetParams[] } +): Promise { + const results: GameAsset[] = []; + + // Process each asset in the batch + for (const assetParams of params.assets) { + // Create and validate the asset + const asset = new GameAsset({ + ...assetParams, + timestamp: Date.now() + }); + await validateOrReject(asset); + + // Store the asset + await putChainObject(ctx, asset); + results.push(asset); + } + + return results; +} +``` + +2. Batch Updates with Validation: +```typescript +@Submit() +async function batchUpdateAssets( + ctx: GalaChainContext, + params: { updates: { id: string; changes: Partial }[] } +): Promise { + const updated: GameAsset[] = []; + const errors: Error[] = []; + + for (const { id, changes } of params.updates) { + try { + const key = ChainObject.getCompositeKeyFromParts( + GameAsset.INDEX_KEY, + [id] + ); + const asset = await getObjectByKey(ctx, GameAsset, key); + + if (!asset) { + throw new Error(`Asset ${id} not found`); + } + + const updatedAsset = new GameAsset({ + ...asset, + ...changes + }); + await validateOrReject(updatedAsset); + await putChainObject(ctx, updatedAsset); + updated.push(updatedAsset); + } catch (error) { + errors.push(error); + } + } + + if (errors.length > 0) { + throw new Error(`Batch update failed: ${errors.map(e => e.message).join(', ')}`); + } + + return updated; +} +``` + +3. Batch Queries with Pagination: +```typescript +@Query() +async function batchGetAssets( + ctx: GalaChainContext, + params: { ids: string[], pageSize?: number } +): Promise<{ assets: GameAsset[]; bookmark?: string }> { + const pageSize = params.pageSize || 100; + let bookmark = ''; + const assets: GameAsset[] = []; + + do { + const { results, metadata } = await getObjectsByPartialCompositeKeyWithPagination( + ctx, + GameAsset.INDEX_KEY, + [], + GameAsset, + pageSize, + bookmark + ); + + // Filter for requested IDs + const matchingAssets = results.filter( + asset => params.ids.includes(asset.id) + ); + assets.push(...matchingAssets); + + bookmark = metadata.bookmark; + } while (bookmark && assets.length < params.ids.length); + + return { assets, bookmark }; +} +``` + +4. Atomic Batch Operations: +```typescript +@Submit() +async function atomicBatchTransfer( + ctx: GalaChainContext, + params: { + fromId: string; + transfers: { toId: string; amount: number }[]; + } +): Promise { + // Get source balance + const fromKey = ChainObject.getCompositeKeyFromParts( + Balance.INDEX_KEY, + [params.fromId] + ); + const fromBalance = await getObjectByKey(ctx, Balance, fromKey); + + if (!fromBalance) { + throw new Error('Source balance not found'); + } + + // Calculate total transfer amount + const totalAmount = params.transfers.reduce( + (sum, t) => sum + t.amount, + 0 + ); + + // Verify sufficient balance + if (fromBalance.amount < totalAmount) { + throw new Error('Insufficient balance for batch transfer'); + } + + // Process all transfers + const updatedBalances: Balance[] = []; + for (const transfer of params.transfers) { + const toKey = ChainObject.getCompositeKeyFromParts( + Balance.INDEX_KEY, + [transfer.toId] + ); + const toBalance = await getObjectByKey(ctx, Balance, toKey) || new Balance({ + id: transfer.toId, + amount: 0 + }); + + // Update balances + toBalance.amount += transfer.amount; + updatedBalances.push(toBalance); + } + + // Update source balance + fromBalance.amount -= totalAmount; + updatedBalances.push(fromBalance); + + // Save all changes + for (const balance of updatedBalances) { + await putChainObject(ctx, balance); + } +} +``` + +Best Practices: +- Validate all items before processing +- Use pagination for large batches +- Implement proper error handling +- Consider atomicity requirements +- Monitor performance impact + +Key Points: +- Batch operations improve throughput +- Handle errors gracefully +- Use pagination for large sets +- Maintain data consistency +- Consider transaction limits + +Performance Tips: +- Optimize batch sizes +- Use parallel processing when possible +- Implement retry mechanisms +- Monitor memory usage +- Cache repeated lookups \ No newline at end of file diff --git a/docs/questions/implement-data-versioning-in-chaincode.md b/docs/questions/implement-data-versioning-in-chaincode.md new file mode 100644 index 0000000000..efef15c1ca --- /dev/null +++ b/docs/questions/implement-data-versioning-in-chaincode.md @@ -0,0 +1,134 @@ +### Question + + +How do I implement data versioning in chaincode? + + +### Answer + + +Data versioning in GalaChain leverages Hyperledger Fabric's built-in versioning capabilities while providing additional patterns for version management. Here's how to work with versioned data: + +1. Using Schema Versions: +```typescript +export class GameAsset extends ChainObject { + @ChainKey({ position: 0 }) + public readonly assetId: string; + + public readonly schemaVersion: number = 2; + + // Version 1 fields + public readonly name: string; + public readonly type: string; + + // Version 2 fields + @IsOptional() + public readonly attributes?: AssetAttributes; + + // Version handling + public static fromPreviousVersion(old: any): GameAsset { + if (!old.schemaVersion || old.schemaVersion === 1) { + return new GameAsset({ + ...old, + schemaVersion: 2, + attributes: { + // Map old fields to new structure + level: 1, + rarity: 'common' + } + }); + } + return old as GameAsset; + } +} +``` + +2. Accessing Version History: +```typescript +@Query() +async function getAssetHistory( + ctx: GalaChainContext, + params: { assetId: string } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + GameAsset.INDEX_KEY, + [params.assetId] + ); + + // Access Fabric's history functionality + const iterator = await ctx.stub.getHistoryForKey(key); + const results = []; + + try { + let result = await iterator.next(); + while (!result.done) { + const { timestamp, value } = result.value; + if (value) { + const historicalData = JSON.parse(value.toString()); + results.push({ + timestamp, + value: historicalData + }); + } + result = await iterator.next(); + } + } finally { + await iterator.close(); + } + + return results; +} +``` + +3. Version-Aware Updates: +```typescript +@Submit() +async function updateAsset( + ctx: GalaChainContext, + params: { assetId: string, updates: Partial } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + GameAsset.INDEX_KEY, + [params.assetId] + ); + + const currentAsset = await getObjectByKey(ctx, GameAsset, key); + if (!currentAsset) { + throw new Error('Asset not found'); + } + + // Handle version differences + const baseAsset = GameAsset.fromPreviousVersion(currentAsset); + + // Apply updates + const updatedAsset = new GameAsset({ + ...baseAsset, + ...params.updates, + schemaVersion: 2 // Ensure latest version + }); + + await putChainObject(ctx, updatedAsset); + return updatedAsset; +} +``` + +Best Practices: +- Track schema versions explicitly +- Provide migration paths between versions +- Handle version differences in queries +- Document version changes +- Test version compatibility + +Key Points: +- Hyperledger Fabric maintains transaction history +- Use schemaVersion for data structure changes +- Implement version migration methods +- Access history through Fabric's APIs +- Consider version compatibility in updates + +Version Management Tips: +- Keep version changes backward compatible when possible +- Implement clear upgrade paths +- Test version migrations thoroughly +- Document version differences +- Monitor version distribution in production \ No newline at end of file diff --git a/docs/questions/implement-integration-tests-in-chaincode.md b/docs/questions/implement-integration-tests-in-chaincode.md new file mode 100644 index 0000000000..deed26363b --- /dev/null +++ b/docs/questions/implement-integration-tests-in-chaincode.md @@ -0,0 +1,188 @@ +### Question + + +How can I implement integration tests for chaincode? + + +### Answer + + +GalaChain uses end-to-end (e2e) tests to verify chaincode behavior against a running chaincode instance. Here's how to implement effective integration tests: + +1. Setting Up E2E Tests: +```typescript +// In your e2e test directory +import { ChainClient, TestUser } from '@gala-chain/client'; +import { GalaChainResponse } from '@gala-chain/api'; + +describe('Token Operations E2E', () => { + let client: ChainClient; + let admin: TestUser; + let user1: TestUser; + + beforeAll(async () => { + // Connect to running chaincode + client = await ChainClient.newClient(); + + // Set up test users + admin = await TestUser.newUser('admin'); + user1 = await TestUser.newUser('user1'); + + // Register users with chaincode + await client.registerUser(admin); + await client.registerUser(user1); + }); +}); +``` + +2. Testing Complete Workflows: +```typescript +it('should complete token lifecycle', async () => { + // Create token class + const createResponse = await client.submit( + 'TokenContract:CreateTokenClass', + { + name: 'Test Token', + symbol: 'TEST', + decimals: 0 + }, + admin + ); + expect(createResponse).toEqual(GalaChainResponse.Success()); + + // Mint tokens + const mintResponse = await client.submit( + 'TokenContract:MintTokens', + { + tokenClass: 'TEST', + amount: '100', + recipient: user1.identityKey + }, + admin + ); + expect(mintResponse).toEqual(GalaChainResponse.Success()); + + // Query balance + const balanceResponse = await client.evaluate( + 'TokenContract:GetBalance', + { + tokenClass: 'TEST', + owner: user1.identityKey + } + ); + expect(balanceResponse.payload.amount).toEqual('100'); +}); +``` + +3. Testing Error Scenarios: +```typescript +it('should handle invalid operations', async () => { + // Attempt to mint without permission + const response = await client.submit( + 'TokenContract:MintTokens', + { + tokenClass: 'TEST', + amount: '100', + recipient: user1.identityKey + }, + user1 // Non-admin user + ); + + expect(response).toEqual( + GalaChainResponse.Error('UNAUTHORIZED') + ); +}); +``` + +4. Testing State Changes: +```typescript +it('should update state correctly', async () => { + // Get initial state + const initialState = await client.evaluate( + 'TokenContract:GetState', + { key: 'test-key' } + ); + + // Perform operation + await client.submit( + 'TokenContract:UpdateState', + { key: 'test-key', value: 'new-value' }, + admin + ); + + // Verify state change + const updatedState = await client.evaluate( + 'TokenContract:GetState', + { key: 'test-key' } + ); + expect(updatedState).not.toEqual(initialState); +}); +``` + +5. Testing Multi-User Scenarios: +```typescript +it('should handle concurrent users', async () => { + const user2 = await TestUser.newUser('user2'); + await client.registerUser(user2); + + // Set up initial state + await client.submit( + 'TokenContract:MintTokens', + { + tokenClass: 'TEST', + amount: '100', + recipient: user1.identityKey + }, + admin + ); + + // Perform concurrent operations + const [transfer1, transfer2] = await Promise.all([ + client.submit( + 'TokenContract:Transfer', + { + tokenClass: 'TEST', + amount: '60', + recipient: user2.identityKey + }, + user1 + ), + client.submit( + 'TokenContract:Transfer', + { + tokenClass: 'TEST', + amount: '60', + recipient: user2.identityKey + }, + user1 + ) + ]); + + // Verify only one transfer succeeded + expect( + transfer1.isSuccess() && !transfer2.isSuccess() || + !transfer1.isSuccess() && transfer2.isSuccess() + ).toBeTruthy(); +}); +``` + +Best Practices: +- Test against running chaincode +- Use ChainClient for interactions +- Test complete workflows +- Verify state changes +- Test multi-user scenarios + +Key Points: +- Tests live in e2e directories +- Use @gala-chain/client +- Test real chaincode instances +- Verify actual state changes +- Test concurrent operations + +Testing Tips: +- Clean up test data +- Use unique test identifiers +- Test error conditions +- Verify state consistency +- Document test scenarios \ No newline at end of file diff --git a/docs/questions/implement-pagination-for-large-data-sets.md b/docs/questions/implement-pagination-for-large-data-sets.md new file mode 100644 index 0000000000..470eda8ae7 --- /dev/null +++ b/docs/questions/implement-pagination-for-large-data-sets.md @@ -0,0 +1,97 @@ +### Question + + +How do I implement pagination for large data sets in chaincode? + + +### Answer + + +Implementing pagination in GalaChain is crucial for handling large datasets efficiently. Here's how to do it: + +1. First, structure your data class with ordered `@ChainKey` decorators for progressive querying: +```typescript +export class GameAsset extends ChainObject { + @Exclude() + public static readonly INDEX_KEY = 'GAME_ASSET'; + + @ChainKey({ position: 0 }) // Most general query key + @IsNotEmpty() + public readonly gameId: string; + + @ChainKey({ position: 1 }) // Secondary filter + @IsNotEmpty() + public readonly assetType: string; + + @ChainKey({ position: 2 }) // Most specific filter + @IsUserAlias() + public readonly owner: UserAlias; + + // Other properties... +} +``` + +2. Implement paginated queries using `getObjectsByPartialCompositeKeyWithPagination`: +```typescript +@Evaluate() // IMPORTANT: Pagination only works in EVALUATE methods +async function queryGameAssets( + ctx: GalaChainContext, + params: { + gameId: string; + assetType?: string; // Optional for progressive filtering + pageSize: number; + bookmark: string; + } +): Promise<{ + results: GameAsset[]; + metadata: { bookmark: string; count: number }; +}> { + // Build partial key based on provided filters + const attributes = [params.gameId]; + if (params.assetType) { + attributes.push(params.assetType); + } + + return getObjectsByPartialCompositeKeyWithPagination( + ctx, + GameAsset.INDEX_KEY, + attributes, + GameAsset, + params.pageSize, + params.bookmark + ); +} +``` + +3. Handle pagination in your client code: +```typescript +async function fetchAllAssets(ctx: GalaChainContext, gameId: string) { + let bookmark = ''; + const PAGE_SIZE = 20; + const allResults: GameAsset[] = []; + + do { + const { results, metadata } = await queryGameAssets(ctx, { + gameId, + pageSize: PAGE_SIZE, + bookmark + }); + + allResults.push(...results); + bookmark = metadata.bookmark; + } while (bookmark); // Empty bookmark means end of results +} +``` + +Important considerations: +- Only use pagination in `@Evaluate()` methods, not in `@Submit()` methods +- Non-paginated queries may timeout or error when hitting max page limits +- Chain key order enables progressive filtering (e.g., query by game, then game+type) +- Use small page sizes (20-50) to avoid timeout issues +- Empty bookmark indicates end of results + +Best practices: +- Always implement pagination for queries that could return large result sets +- Structure chain keys from most general to most specific for flexible querying +- Cache results client-side when fetching multiple pages +- Consider implementing server-side cursors for very large datasets \ No newline at end of file diff --git a/docs/questions/implement-rate-limiting-in-chaincode.md b/docs/questions/implement-rate-limiting-in-chaincode.md new file mode 100644 index 0000000000..e1c56257f9 --- /dev/null +++ b/docs/questions/implement-rate-limiting-in-chaincode.md @@ -0,0 +1,30 @@ +### Question + + +How do I implement rate limiting in chaincode? + + +### Answer + + +Rate limiting in GalaChain can be implemented at multiple levels. Here's a comprehensive guide: + +1. Infrastructure Level (Recommended): + - Use GalaChain Gateway's built-in rate limiting + - Configure limits in network policy + - Leverage Hyperledger Fabric's built-in controls + +2. Chaincode implementation possibilities: + - Use sliding windows for rate limiting + - Implement per-user and per-operation limits + - Consider resource costs in limit calculations + - Clean up expired rate limit records + - Log rate limit violations + +3. Client-Side Considerations: + - Implement exponential backoff + - Handle rate limit errors gracefully + - Cache responses when possible + - Monitor rate limit usage + +Note: While chaincode-level rate limiting is possible, it's generally better to implement rate limiting at the infrastructure level using GalaChain Gateway's built-in features. This provides better performance and more consistent enforcement. \ No newline at end of file diff --git a/docs/questions/implement-retry-mechanisms-in-chaincode.md b/docs/questions/implement-retry-mechanisms-in-chaincode.md new file mode 100644 index 0000000000..bd6062e0fd --- /dev/null +++ b/docs/questions/implement-retry-mechanisms-in-chaincode.md @@ -0,0 +1,43 @@ +### Question + + +How can I implement retry mechanisms in chaincode? + + +### Answer + + +GalaChain provides built-in mechanisms for handling retries effectively. Here's what you need to know: + +1. GalaChainStubCache Atomic Operations: +- All chaincode operations in GalaChain are atomic by default through the `GalaChainStubCache` +- The cache ensures that all state changes within a transaction are applied together or not at all +- There's no need to implement custom transaction management or retry logic within chaincode +- State changes are only committed when the transaction successfully completes + +2. Operations API Retry Mechanism: +- GalaChain's Operations API includes automatic retry logic for failed external API calls +- Specific errors, including MVCC_READ_CONFLICT, trigger automatic retries +- The API will retry the failed operation using the same inputs +- This ensures consistency and reliability without requiring custom retry implementation + +Key Points: +- Don't implement retry logic in chaincode +- Let GalaChain handle retries automatically +- Trust the built-in atomicity guarantees +- Use the Operations API for external calls +- Focus on business logic implementation + +Best Practices: +- Write chaincode assuming atomic execution +- Let the platform handle retries +- Handle business-level errors appropriately +- Log errors for monitoring +- Trust the platform's retry mechanisms + +Important Notes: +- Custom retry logic may interfere with GalaChain's mechanisms +- The platform handles concurrency and conflicts +- Focus on error handling rather than retries +- Let the Operations API manage external retries +- Maintain idempotent operations \ No newline at end of file diff --git a/docs/questions/implement-rich-queries-using-couchdb.md b/docs/questions/implement-rich-queries-using-couchdb.md new file mode 100644 index 0000000000..cbc150c621 --- /dev/null +++ b/docs/questions/implement-rich-queries-using-couchdb.md @@ -0,0 +1,82 @@ +### Question + + +How do I implement rich queries using CouchDB in chaincode? + + +### Answer + + +While Hyperledger Fabric supports both LevelDB and CouchDB, the GalaChain SDK is designed to work with LevelDB as its underlying state database. Here's how to effectively query data in GalaChain: + +1. Use Composite Keys for Efficient Queries: +```typescript +export class GameAsset extends ChainObject { + @Exclude() + public static readonly INDEX_KEY = 'GAME_ASSET'; + + @ChainKey({ position: 0 }) + public readonly gameId: string; + + @ChainKey({ position: 1 }) + public readonly assetType: string; + + // Non-key fields + public readonly metadata: AssetMetadata; +} +``` + +2. Query Using Partial Composite Keys: +```typescript +// Get all assets for a game +const gameAssets = await getObjectsByPartialCompositeKey( + ctx, + GameAsset.INDEX_KEY, + [gameId], // Partial key + GameAsset +); + +// Get assets of specific type for a game +const typeAssets = await getObjectsByPartialCompositeKey( + ctx, + GameAsset.INDEX_KEY, + [gameId, assetType], // More specific key + GameAsset +); +``` + +3. Use Pagination for Large Result Sets: +```typescript +const { results, metadata } = await getObjectsByPartialCompositeKeyWithPagination( + ctx, + GameAsset.INDEX_KEY, + [gameId], + GameAsset, + pageSize, + bookmark +); +``` + +4. Implement Custom Filtering in Memory: +```typescript +// Get assets and filter in memory +const assets = await getObjectsByPartialCompositeKey(ctx, GameAsset.INDEX_KEY, [gameId]); +const filteredAssets = assets.filter(asset => + asset.metadata.rarity === 'legendary' && + asset.metadata.level >= 10 +); +``` + +Best Practices: +- Design composite keys to support your most common query patterns +- Order ChainKey positions from most to least frequently queried +- Use pagination for large result sets +- Keep in mind that complex filtering happens in memory +- Consider data volume when designing queries + +Key Points: +- LevelDB is the supported state database +- Queries are based on composite key ranges +- Complex queries require client-side filtering +- No support for rich queries or indexes like in CouchDB +- Performance depends on composite key design \ No newline at end of file diff --git a/docs/questions/logging-best-practices.md b/docs/questions/logging-best-practices.md new file mode 100644 index 0000000000..2c15ffc14d --- /dev/null +++ b/docs/questions/logging-best-practices.md @@ -0,0 +1,221 @@ +### Question + + +What are the best practices for logging in chaincode? + + +### Answer + + +GalaChain provides structured logging through the `GalaLoggerInstance`. Here's how to implement effective logging: + +1. Using the Logger: +```typescript +@Submit() +async function transferTokens( + ctx: GalaChainContext, + params: TransferParams +): Promise { + // Log method entry with parameters + ctx.logger.info('Starting token transfer', { + from: params.fromId, + to: params.toId, + amount: params.amount + }); + + try { + // Log important state checks + ctx.logger.debug('Checking source balance', { + userId: params.fromId + }); + + const balance = await getBalance(ctx, params.fromId); + if (!balance) { + ctx.logger.error('Balance not found', { + userId: params.fromId + }); + throw new Error('Balance not found'); + } + + // Log performance metrics + const startTime = Date.now(); + await processTransfer(ctx, params); + ctx.logger.info('Transfer completed', { + duration: Date.now() - startTime, + success: true + }); + } catch (error) { + // Log errors with context + ctx.logger.error('Transfer failed', { + error: error.message, + code: error.code, + stack: error.stack + }); + throw error; + } +} +``` + +2. Log Levels and Usage: +```typescript +// ERROR: For failures that need immediate attention +ctx.logger.error('Critical system error', { + error: error.message, + component: 'TokenContract', + severity: 'critical' +}); + +// WARN: For potentially harmful situations +ctx.logger.warn('Low balance warning', { + userId: user.id, + balance: balance.amount, + threshold: MIN_BALANCE +}); + +// INFO: For general operational events +ctx.logger.info('User registered', { + userId: user.id, + timestamp: Date.now() +}); + +// DEBUG: For detailed information +ctx.logger.debug('Processing transaction', { + txId: ctx.stub.getTxID(), + chainId: ctx.stub.getChannelID() +}); +``` + +3. Structured Logging: +```typescript +// Define log structure +interface TransferLog { + fromId: string; + toId: string; + amount: number; + success: boolean; + duration?: number; + error?: string; +} + +// Log with structured data +function logTransfer( + ctx: GalaChainContext, + data: TransferLog +): void { + if (data.success) { + ctx.logger.info('Transfer successful', { + operation: 'transfer', + ...data + }); + } else { + ctx.logger.error('Transfer failed', { + operation: 'transfer', + ...data + }); + } +} + +// Use in chaincode +@Submit() +async function transfer( + ctx: GalaChainContext, + params: TransferParams +): Promise { + const startTime = Date.now(); + try { + await processTransfer(ctx, params); + logTransfer(ctx, { + fromId: params.fromId, + toId: params.toId, + amount: params.amount, + success: true, + duration: Date.now() - startTime + }); + } catch (error) { + logTransfer(ctx, { + fromId: params.fromId, + toId: params.toId, + amount: params.amount, + success: false, + error: error.message + }); + throw error; + } +} +``` + +4. Performance Logging: +```typescript +class PerformanceLogger { + private startTime: number; + private checkpoints: Map; + + constructor( + private ctx: GalaChainContext, + private operation: string + ) { + this.startTime = Date.now(); + this.checkpoints = new Map(); + } + + checkpoint(name: string): void { + this.checkpoints.set(name, Date.now()); + } + + end(): void { + const endTime = Date.now(); + const durations = {}; + + this.checkpoints.forEach((time, name) => { + durations[name] = time - this.startTime; + }); + + this.ctx.logger.info('Operation completed', { + operation: this.operation, + totalDuration: endTime - this.startTime, + checkpoints: durations + }); + } +} + +// Usage in chaincode +@Submit() +async function complexOperation( + ctx: GalaChainContext, + params: OperationParams +): Promise { + const perfLogger = new PerformanceLogger(ctx, 'complexOperation'); + + await step1(); + perfLogger.checkpoint('step1'); + + await step2(); + perfLogger.checkpoint('step2'); + + await step3(); + perfLogger.checkpoint('step3'); + + perfLogger.end(); +} +``` + +Best Practices: +- Use appropriate log levels +- Include relevant context +- Structure log data +- Log method entry/exit +- Track performance metrics + +Key Points: +- Use ctx.logger consistently +- Include error details +- Log state transitions +- Monitor performance +- Structure log data + +Logging Tips: +- Keep logs actionable +- Include transaction IDs +- Log security events +- Monitor log volume +- Use consistent formats \ No newline at end of file diff --git a/docs/questions/optimizie-query-performance-in-chaincode.md b/docs/questions/optimizie-query-performance-in-chaincode.md new file mode 100644 index 0000000000..3fd5ce4f16 --- /dev/null +++ b/docs/questions/optimizie-query-performance-in-chaincode.md @@ -0,0 +1,198 @@ +### Question + + +How can I optimize query performance in chaincode? + + +### Answer + + +Here are key strategies to optimize query performance in GalaChain chaincode: + +1. Efficient Key Design: +```typescript +export class GameAsset extends ChainObject { + // Composite key for efficient querying + @ChainKey({ position: 0 }) + public readonly category: string; + + @ChainKey({ position: 1 }) + public readonly type: string; + + @ChainKey({ position: 2 }) + public readonly id: string; + + // Other fields... + public readonly name: string; + public readonly metadata: AssetMetadata; +} + +// Query by category and type +async function getAssetsByType( + ctx: GalaChainContext, + category: string, + type: string +): Promise { + return getObjectsByPartialCompositeKey( + ctx, + GameAsset.INDEX_KEY, + [category, type] + ); +} +``` + +2. Pagination Implementation: +```typescript +@Query() +async function getAssetsWithPagination( + ctx: GalaChainContext, + params: { + pageSize: number; + bookmark?: string; + } +): Promise<{ + assets: GameAsset[]; + bookmark: string; +}> { + const { results, metadata } = await getObjectsByPartialCompositeKeyWithPagination( + ctx, + GameAsset.INDEX_KEY, + [], + GameAsset, + params.pageSize, + params.bookmark + ); + + return { + assets: results, + bookmark: metadata.bookmark + }; +} +``` + +3. Targeted Queries: +```typescript +// BAD: Fetches all assets then filters +async function badQueryImplementation( + ctx: GalaChainContext, + type: string +): Promise { + const allAssets = await getObjectsByPartialCompositeKey( + ctx, + GameAsset.INDEX_KEY, + [] + ); + return allAssets.filter(asset => asset.type === type); +} + +// GOOD: Uses composite key to filter at query time +async function goodQueryImplementation( + ctx: GalaChainContext, + type: string +): Promise { + return getObjectsByPartialCompositeKey( + ctx, + GameAsset.INDEX_KEY, + [type] + ); +} +``` + +4. Batch Processing: +```typescript +@Query() +async function batchGetAssets( + ctx: GalaChainContext, + params: { ids: string[] } +): Promise { + const assets: GameAsset[] = []; + const batchSize = 100; + + // Process in batches + for (let i = 0; i < params.ids.length; i += batchSize) { + const batchIds = params.ids.slice(i, i + batchSize); + const batchPromises = batchIds.map(id => + getObjectByKey( + ctx, + GameAsset, + ChainObject.getCompositeKeyFromParts( + GameAsset.INDEX_KEY, + [id] + ) + ) + ); + + const batchResults = await Promise.all(batchPromises); + assets.push(...batchResults.filter(Boolean)); + } + + return assets; +} +``` + +5. Caching Results: +```typescript +export class QueryCache extends ChainObject { + @ChainKey({ position: 0 }) + public readonly queryKey: string; + + public readonly results: any[]; + public readonly timestamp: number; + public readonly expiresIn: number; + + public isExpired(): boolean { + return Date.now() > this.timestamp + this.expiresIn; + } +} + +async function getCachedQuery( + ctx: GalaChainContext, + queryKey: string, + queryFn: () => Promise, + expiresIn: number = 300000 // 5 minutes +): Promise { + // Try to get cached results + const cacheKey = ChainObject.getCompositeKeyFromParts( + QueryCache.INDEX_KEY, + [queryKey] + ); + const cached = await getObjectByKey(ctx, QueryCache, cacheKey); + + if (cached && !cached.isExpired()) { + return cached.results; + } + + // Execute query and cache results + const results = await queryFn(); + const cache = new QueryCache({ + queryKey, + results, + timestamp: Date.now(), + expiresIn + }); + + await putChainObject(ctx, cache); + return results; +} +``` + +Best Practices: +- Design composite keys for common query patterns +- Implement pagination for large result sets +- Use targeted queries instead of filtering +- Process large queries in batches +- Cache frequently accessed results + +Key Points: +- Composite keys enable efficient lookups +- Pagination prevents memory issues +- Targeted queries reduce network load +- Batch processing improves throughput +- Caching reduces redundant queries + +Performance Tips: +- Minimize full-scan operations +- Use appropriate page sizes +- Implement query timeouts +- Monitor query patterns +- Optimize key structures \ No newline at end of file diff --git a/docs/questions/store-and-query-data-in-chaincode.md b/docs/questions/store-and-query-data-in-chaincode.md new file mode 100644 index 0000000000..37f61dd2f6 --- /dev/null +++ b/docs/questions/store-and-query-data-in-chaincode.md @@ -0,0 +1,81 @@ +### Question + + +How do I store and query data in chaincode? + + +### Answer + + +To store and query data in GalaChain chaincode, follow these steps: + +1. First, create a class that extends `ChainObject` and define your data structure: +```typescript +import { ChainObject, ChainKey, IsNotEmpty, IsUserAlias } from '@gala-chain/api'; + +export class MyData extends ChainObject { + @Exclude() + public static readonly INDEX_KEY = 'MYDATA'; // Used for queries + + @ChainKey({ position: 0 }) // Order matters for queries + @IsUserAlias() + public readonly owner: UserAlias; + + @ChainKey({ position: 1 }) + @IsNotEmpty() + public readonly dataType: string; + + // Add other properties as needed + public readonly value: string; +} +``` + +2. Store data using `putChainObject`: +```typescript +async function storeData(ctx: GalaChainContext, data: MyData): Promise { + // Validates and writes to state + await putChainObject(ctx, data); +} +``` + +3. Query data using composite keys: +```typescript +async function queryByOwner( + ctx: GalaChainContext, + owner: UserAlias +): Promise { + // Query by partial key (owner) + return getObjectsByPartialCompositeKey( + ctx, + MyData.INDEX_KEY, // The index key defined in class + [owner], // Partial key attributes + MyData // Constructor for deserialization + ); +} +``` + +4. For large result sets, use pagination: +```typescript +async function queryWithPagination( + ctx: GalaChainContext, + owner: UserAlias, + pageSize: number, + bookmark: string +) { + return getObjectsByPartialCompositeKeyWithPagination( + ctx, + MyData.INDEX_KEY, + [owner], + MyData, + pageSize, + bookmark + ); +} +``` + +Key points: +- Use `@ChainKey` decorators to define composite key parts +- Order of `@ChainKey` positions affects query capabilities +- Use `putChainObject` for validation and storage +- Use `getObjectsByPartialCompositeKey` for queries +- Consider pagination for large result sets \ No newline at end of file diff --git a/docs/questions/unit-tests-for-chaincode.md b/docs/questions/unit-tests-for-chaincode.md new file mode 100644 index 0000000000..89c8901551 --- /dev/null +++ b/docs/questions/unit-tests-for-chaincode.md @@ -0,0 +1,166 @@ +### Question + + +How do I write unit tests for chaincode? + + +### Answer + + +GalaChain provides a robust testing framework that makes it easy to write unit tests for chaincode. Here's how to write effective tests: + +1. Basic Test Structure: +```typescript +import { GalaChainResponse, createValidSubmitDTO } from '@gala-chain/api'; +import { fixture, users, writesMap } from '@gala-chain/test'; +import GalaChainTokenContract from '../__test__/GalaChainTokenContract'; + +describe('MyChaincode', () => { + it('should perform an operation', async () => { + // Given + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.testUser1) + .savedState(/* initial state objects */); + + const dto = await createValidSubmitDTO(MyOperationDto, { + // operation parameters + }).signed(users.testUser1.privateKey); + + // When + const response = await contract.MyOperation(ctx, dto); + + // Then + expect(response).toEqual(GalaChainResponse.Success([/* expected result */])); + expect(getWrites()).toEqual(writesMap(/* expected state changes */)); + }); +}); +``` + +2. Testing State Changes: +```typescript +import { TokenBalance, TokenInstance } from '@gala-chain/api'; + +it('should update token balance', async () => { + // Given + const tokenInstance = await createValidChainObject(TokenInstance, { + owner: users.testUser1.identityKey, + // other token properties + }); + + const initialBalance = new TokenBalance({ + owner: users.testUser1.identityKey, + quantity: new BigNumber('100') + }); + + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .savedState(tokenInstance, initialBalance); + + // When + const response = await contract.TransferToken(ctx, transferDto); + + // Then + const expectedBalance = new TokenBalance({ + owner: users.testUser1.identityKey, + quantity: new BigNumber('50') + }); + + expect(getWrites()).toEqual(writesMap(expectedBalance)); +}); +``` + +3. Testing Error Cases: +```typescript +it('should fail with insufficient balance', async () => { + // Given + const balance = new TokenBalance({ + owner: users.testUser1.identityKey, + quantity: new BigNumber('10') + }); + + const { ctx, contract } = fixture(GalaChainTokenContract) + .savedState(balance); + + const dto = await createValidSubmitDTO(TransferDto, { + amount: new BigNumber('20') + }).signed(users.testUser1.privateKey); + + // When + const response = await contract.Transfer(ctx, dto); + + // Then + expect(response).toEqual( + GalaChainResponse.Error( + new InsufficientBalanceError( + users.testUser1.identityKey, + new BigNumber('20'), + new BigNumber('10') + ) + ) + ); +}); +``` + +4. Testing Validation: +```typescript +it('should validate input parameters', async () => { + // Given + const { ctx, contract } = fixture(GalaChainTokenContract); + + const invalidDto = await createValidSubmitDTO(MintTokenDto, { + quantity: new BigNumber('-1') // Invalid negative quantity + }).signed(users.testUser1.privateKey); + + // When + const response = await contract.MintToken(ctx, invalidDto); + + // Then + expect(response).toEqual( + GalaChainResponse.Error( + new ValidationError(['quantity must be a positive number']) + ) + ); +}); +``` + +5. Testing Complex Scenarios: +```typescript +it('should handle multiple operations in sequence', async () => { + // Given + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.testUser1, users.testUser2) + .savedState(initialState); + + // When - First operation + await contract.Operation1(ctx, dto1); + const intermediateState = getWrites(); + + // When - Second operation + await contract.Operation2(ctx, dto2); + const finalState = getWrites(); + + // Then + expect(intermediateState).toEqual(writesMap(expectedIntermediateState)); + expect(finalState).toEqual(writesMap(expectedFinalState)); +}); +``` + +Best Practices: +- Use descriptive test names +- Follow Given-When-Then pattern +- Test both success and error cases +- Verify state changes +- Test input validation + +Key Points: +- Use fixture helper for setup +- Verify GalaChainResponse +- Check state modifications +- Test error conditions +- Validate complex workflows + +Testing Tips: +- Mock external dependencies +- Test edge cases +- Group related tests +- Keep tests focused +- Use test utilities \ No newline at end of file diff --git a/package.json b/package.json index 91465c5c7c..46eb1e8c9c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "nx run-many -t test", "set-version": "node unifyVersions.js", "licenses": "license-checker --csv --customPath ./licenses/format.json > ./licenses/licenses.csv", - "training-files": "ts-node ./tools/scripts/training-files.ts", + "training-files": "ts-node ./tools/scripts/training-files.ts && ts-node ./tools/scripts/questions-to-jsonl.ts", "typedoc-chain-api": "typedoc --tsconfig ./tsconfig.docs.json --hideGenerator --plugin typedoc-plugin-markdown --githubPages false --out ./docs/chain-api-docs ./chain-api/src && rm ./docs/chain-api-docs/README.md", "typedoc-chain-client": "typedoc --tsconfig ./tsconfig.docs.json --hideGenerator --plugin typedoc-plugin-markdown --githubPages false --out ./docs/chain-client-docs ./chain-client/src && rm ./docs/chain-client-docs/README.md", "typedoc-chaincode": "typedoc --tsconfig ./tsconfig.docs.json --hideGenerator --plugin typedoc-plugin-markdown --githubPages false --out ./docs/chaincode-docs ./chaincode/src && rm ./docs/chaincode-docs/README.md", diff --git a/tools/scripts/questions-to-jsonl.ts b/tools/scripts/questions-to-jsonl.ts new file mode 100644 index 0000000000..0802743956 --- /dev/null +++ b/tools/scripts/questions-to-jsonl.ts @@ -0,0 +1,117 @@ +import * as fs from "fs/promises"; +import * as path from "path"; + +const sourceDir = path.resolve(__dirname, "..", ".."); +const questionsDir = path.resolve(sourceDir, "docs", "questions"); +const chatmlDir = path.resolve(__dirname, "..", "training-data", "chatml"); + +const systemRoleContent = "You are a GalaChain SDK expert assistant, helping developers write " + + "chaincode, use the development environment, and integrate with GalaChain. " + + "You provide accurate, concise guidance based on the official " + + " SDK documentation and best practices."; + +interface Message { + role: "system" | "user" | "assistant"; + content: string; +} + +interface ChatEntry { + messages: Message[]; + format: string; +} + +async function parseMarkdownFile(filePath: string): Promise { + try { + const content = await fs.readFile(filePath, "utf-8"); + const lines = content.split("\n"); + + let questionContent = ""; + let answerContent = ""; + let currentSection = ""; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith("### Question")) { + currentSection = "question"; + continue; + } else if (line.startsWith("### Answer")) { + currentSection = "answer"; + continue; + } + + if (currentSection === "question") { + questionContent += line + "\n"; + } else if (currentSection === "answer") { + answerContent += line + "\n"; + } + } + + // Clean up the content + questionContent = questionContent.trim(); + answerContent = answerContent.trim(); + + if (!questionContent || !answerContent) { + console.warn(`Skipping ${filePath}: Missing question or answer content`); + return null; + } + + return { + messages: [ + { + role: "system", + content: systemRoleContent + }, + { + role: "user", + content: questionContent + }, + { + role: "assistant", + content: answerContent + } + ], + format: "chatml" + }; + } catch (error) { + console.error(`Error parsing file ${filePath}:`, error); + return null; + } +} + +async function processMarkdownToJsonl(outputFilePath: string): Promise { + try { + // Ensure output directory exists + await fs.mkdir(path.dirname(outputFilePath), { recursive: true }); + + // Get all markdown files + const files = await fs.readdir(questionsDir); + const markdownFiles = files.filter(file => file.endsWith(".md")); + + // Process each file and collect entries + const entries: ChatEntry[] = []; + for (const file of markdownFiles) { + const filePath = path.join(questionsDir, file); + const entry = await parseMarkdownFile(filePath); + if (entry) { + entries.push(entry); + } + } + + // Write entries to JSONL file + const jsonlContent = entries.map(entry => JSON.stringify(entry)).join("\n"); + await fs.writeFile(outputFilePath, jsonlContent + "\n", "utf-8"); + + console.log(`Successfully processed ${entries.length} files to ${outputFilePath}`); + } catch (error) { + console.error("Error processing files:", error); + throw error; + } +} + +// Execute the script +const outputFilePath = path.join(chatmlDir, "questions.jsonl"); +processMarkdownToJsonl(outputFilePath) + .catch(error => { + console.error("Script failed:", error); + process.exit(1); + }); From a75bc58b669912e50b38baa570784d0cc8bc606c Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Wed, 9 Apr 2025 20:31:51 -0700 Subject: [PATCH 02/12] chore: add copyright header --- tools/scripts/questions-to-jsonl.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tools/scripts/questions-to-jsonl.ts b/tools/scripts/questions-to-jsonl.ts index 0802743956..2cf771a9b4 100644 --- a/tools/scripts/questions-to-jsonl.ts +++ b/tools/scripts/questions-to-jsonl.ts @@ -1,3 +1,17 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import * as fs from "fs/promises"; import * as path from "path"; From 85902039ce2c24025da6a7166c3c987e96f6f348 Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Wed, 9 Apr 2025 20:41:59 -0700 Subject: [PATCH 03/12] chore: lint --- tools/scripts/publish.mjs | 9 ++++----- tools/scripts/questions-to-jsonl.ts | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/tools/scripts/publish.mjs b/tools/scripts/publish.mjs index 93e013fc6b..821e0e0823 100644 --- a/tools/scripts/publish.mjs +++ b/tools/scripts/publish.mjs @@ -21,11 +21,10 @@ * * You might need to authenticate with NPM before running this script. */ +import devkit from "@nx/devkit"; +import { execSync } from "child_process"; +import { readFileSync, writeFileSync } from "fs"; -import { execSync } from 'child_process'; -import { readFileSync, writeFileSync } from 'fs'; - -import devkit from '@nx/devkit'; const { readCachedProjectGraph } = devkit; function invariant(condition, message) { @@ -37,7 +36,7 @@ function invariant(condition, message) { // Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag} // Default "tag" to "next" so we won't publish the "latest" tag by accident. -const [, , name, version, tag = 'next'] = process.argv; +const [, , name, version, tag = "next"] = process.argv; // A simple SemVer validation to validate the version const validVersion = /^\d+\.\d+\.\d+(-\w+\.\d+)?/; diff --git a/tools/scripts/questions-to-jsonl.ts b/tools/scripts/questions-to-jsonl.ts index 2cf771a9b4..2998e69d47 100644 --- a/tools/scripts/questions-to-jsonl.ts +++ b/tools/scripts/questions-to-jsonl.ts @@ -19,9 +19,10 @@ const sourceDir = path.resolve(__dirname, "..", ".."); const questionsDir = path.resolve(sourceDir, "docs", "questions"); const chatmlDir = path.resolve(__dirname, "..", "training-data", "chatml"); -const systemRoleContent = "You are a GalaChain SDK expert assistant, helping developers write " + - "chaincode, use the development environment, and integrate with GalaChain. " + - "You provide accurate, concise guidance based on the official " + +const systemRoleContent = + "You are a GalaChain SDK expert assistant, helping developers write " + + "chaincode, use the development environment, and integrate with GalaChain. " + + "You provide accurate, concise guidance based on the official " + " SDK documentation and best practices."; interface Message { @@ -99,7 +100,7 @@ async function processMarkdownToJsonl(outputFilePath: string): Promise { // Get all markdown files const files = await fs.readdir(questionsDir); - const markdownFiles = files.filter(file => file.endsWith(".md")); + const markdownFiles = files.filter((file) => file.endsWith(".md")); // Process each file and collect entries const entries: ChatEntry[] = []; @@ -112,7 +113,7 @@ async function processMarkdownToJsonl(outputFilePath: string): Promise { } // Write entries to JSONL file - const jsonlContent = entries.map(entry => JSON.stringify(entry)).join("\n"); + const jsonlContent = entries.map((entry) => JSON.stringify(entry)).join("\n"); await fs.writeFile(outputFilePath, jsonlContent + "\n", "utf-8"); console.log(`Successfully processed ${entries.length} files to ${outputFilePath}`); @@ -124,8 +125,7 @@ async function processMarkdownToJsonl(outputFilePath: string): Promise { // Execute the script const outputFilePath = path.join(chatmlDir, "questions.jsonl"); -processMarkdownToJsonl(outputFilePath) - .catch(error => { - console.error("Script failed:", error); - process.exit(1); - }); +processMarkdownToJsonl(outputFilePath).catch((error) => { + console.error("Script failed:", error); + process.exit(1); +}); From 43a569ce7599c8ae2d19e9e34205561639a8c17f Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Thu, 10 Apr 2025 15:03:17 -0700 Subject: [PATCH 04/12] docs: additional Q&A content --- .../asynchronous-operations-in-chaincode.md | 72 +++++++++++++++++ ...or-securing-sensitive-data-in-chaincode.md | 60 +++++++++++++++ docs/questions/caching-in-chaincode.md | 54 +++++++++++++ .../dependency-management-in-chaincode.md | 41 ++++++++++ .../questions/handle-cross-chaincode-calls.md | 32 ++++++++ ...mplement-custom-decorators-in-chaincode.md | 73 ++++++++++++++++++ .../implement-role-based-access-control.md | 77 +++++++++++++++++++ .../monitor-chaincode-performance.md | 73 ++++++++++++++++++ docs/questions/secure-authentication.md | 57 ++++++++++++++ .../questions/upgrading-chaincode-versions.md | 75 ++++++++++++++++++ 10 files changed, 614 insertions(+) create mode 100644 docs/questions/asynchronous-operations-in-chaincode.md create mode 100644 docs/questions/best-practices-for-securing-sensitive-data-in-chaincode.md create mode 100644 docs/questions/caching-in-chaincode.md create mode 100644 docs/questions/dependency-management-in-chaincode.md create mode 100644 docs/questions/handle-cross-chaincode-calls.md create mode 100644 docs/questions/implement-custom-decorators-in-chaincode.md create mode 100644 docs/questions/implement-role-based-access-control.md create mode 100644 docs/questions/monitor-chaincode-performance.md create mode 100644 docs/questions/secure-authentication.md create mode 100644 docs/questions/upgrading-chaincode-versions.md diff --git a/docs/questions/asynchronous-operations-in-chaincode.md b/docs/questions/asynchronous-operations-in-chaincode.md new file mode 100644 index 0000000000..8f6e091e8b --- /dev/null +++ b/docs/questions/asynchronous-operations-in-chaincode.md @@ -0,0 +1,72 @@ +### Question + + +How can I implement asynchronous operations in chaincode? + + +### Answer + + +In GalaChain, chaincode operations are inherently asynchronous. Here's how to work with async operations effectively: + +1. Basic Async Pattern: +```typescript +class GameContract extends Contract { + @Submit() + async processGameAction( + ctx: GalaChainContext, + params: { gameId: string } + ): Promise { + // Async state operations + const game = await getObjectByKey(ctx, Game, params.gameId); + const player = await getObjectByKey(ctx, Player, ctx.callingUser.id); + + // Process game logic + await this.updateGameState(ctx, game, player); + } + + private async updateGameState( + ctx: GalaChainContext, + game: Game, + player: Player + ): Promise { + // Multiple async operations + await this.updatePlayerStats(ctx, player); + await this.updateGameProgress(ctx, game); + } +} +``` + +2. Best Practices: + - Prefer `async/await` syntax for simplicity and readability + - Handle errors with try/catch blocks + - Avoid using Promise.all for parallel operations! It can have non-deterministic ordering or outcomes when run across multiple peers that can fail in hard-to-troubleshoot ways + - Keep transaction duration reasonable + - Log async operation progress + +3. Error Handling: +```typescript +@Submit() +async function processWithRetry( + ctx: GalaChainContext, + params: any +): Promise { + try { + await this.performAsyncOperation(ctx, params); + } catch (error) { + ctx.logger.error('Operation failed', { + error: error.message, + params + }); + throw error; // Rollback transaction + } +} +``` + +4. Important Considerations: + - All chaincode operations must complete within transaction timeout + - External async calls (HTTP, etc.) are not allowed + - State changes are only committed at transaction end + - Use proper error handling for rollbacks + +Note: While chaincode operations are async, they must be deterministic and complete within the transaction boundary. Long-running operations should be broken into multiple transactions. \ No newline at end of file diff --git a/docs/questions/best-practices-for-securing-sensitive-data-in-chaincode.md b/docs/questions/best-practices-for-securing-sensitive-data-in-chaincode.md new file mode 100644 index 0000000000..be5d5c7bb2 --- /dev/null +++ b/docs/questions/best-practices-for-securing-sensitive-data-in-chaincode.md @@ -0,0 +1,60 @@ +### Question + + +What are the best practices for securing sensitive data in chaincode? + + +### Answer + + +When working with sensitive data in GalaChain, it's crucial to understand that all data stored on the blockchain is immutable and visible to all network participants. Here are the key principles and practices: + +1. Data Storage Guidelines: + - Never store private keys, passwords, or API keys on chain + - Avoid storing personally identifiable information (PII) + - Store only hashes of sensitive documents, not the documents themselves + - Use off-chain storage for sensitive data with only references on chain + +2. Data Privacy: + - Use private data collections for data that should only be accessible to specific organizations + - Implement proper access controls using `ctx.callingUser` checks + - Consider using encryption for sensitive fields that must be stored on chain + - Remember that encrypted data on chain is still visible and immutable + +3. Example Implementation: +```typescript +class SecureContract extends Contract { + @Submit() + async storeDocumentReference( + ctx: GalaChainContext, + params: { + documentHash: string, // Hash of the actual document + storageReference: string // Reference to off-chain storage + } + ): Promise { + // Verify caller has permission + if (!ctx.callingUser.hasRole('DOCUMENT_MANAGER')) { + throw new Error('Insufficient permissions'); + } + + // Store only the hash and reference + const documentRef = new DocumentReference({ + hash: params.documentHash, + reference: params.storageReference, + owner: ctx.callingUser.id, + timestamp: Date.now() + }); + + await putChainObject(ctx, documentRef); + } +} +``` + +4. Security Best Practices: + - Validate all input data thoroughly + - Log access to sensitive data for audit purposes + - Use secure off-chain storage solutions for sensitive data + - Implement proper key management for any encryption keys + - Regular security audits of chaincode + +Remember: Once data is written to the blockchain, it cannot be removed or modified. Always carefully consider what data should be stored on chain versus off chain. \ No newline at end of file diff --git a/docs/questions/caching-in-chaincode.md b/docs/questions/caching-in-chaincode.md new file mode 100644 index 0000000000..e8e6a892c6 --- /dev/null +++ b/docs/questions/caching-in-chaincode.md @@ -0,0 +1,54 @@ +### Question + + +How do I implement caching in chaincode? + + +### Answer + + +Caching in GalaChain is automatically handled by the `GalaChainStubCache`. Here's what you need to know: + +1. Automatic Caching: + - The SDK handles caching automatically + - No manual cache implementation required + - Works within transaction boundaries + - Optimizes frequent reads of the same data + +2. Using the Cache: +```typescript +class GameContract extends Contract { + @Submit() + async processGameItems( + ctx: GalaChainContext, + params: { itemId: string } + ): Promise { + // These calls automatically use the stub cache + const item = await getObjectByKey(ctx, GameItem, params.itemId); + + // Multiple reads of the same item use cache + const sameItem = await getObjectByKey(ctx, GameItem, params.itemId); + + // Range queries also use cache + const items = await getObjectsByPartialCompositeKey( + ctx, + GameItem.INDEX_KEY, + [item.owner] + ); + } +} +``` + +3. How It Works: + - Reads are cached in memory during transaction + - Writes are held in cache until transaction completion + - Cache is automatically flushed at transaction end + - All updates are written together + +4. Benefits: + - Improved performance for repeated reads + - Consistent view of state within transaction + - Reduced world state database load + - Automatic cache management + +Note: The stub cache is specifically designed for transaction-level caching. For cross-transaction data access patterns, consider implementing appropriate data structures and query patterns in your chaincode. \ No newline at end of file diff --git a/docs/questions/dependency-management-in-chaincode.md b/docs/questions/dependency-management-in-chaincode.md new file mode 100644 index 0000000000..9e39f2e2e4 --- /dev/null +++ b/docs/questions/dependency-management-in-chaincode.md @@ -0,0 +1,41 @@ +### Question + + +What are the best practices for chaincode dependency management? + + +### Answer + + +Proper dependency management is crucial for GalaChain chaincode. Here's what you need to know: + +1. Version Alignment: +```json +{ + "dependencies": { + "@gala-chain/api": "2.0.0", + "@gala-chain/chaincode": "2.0.0" + } +} +``` + +2. Key Requirements: + - All GalaChain packages should use the same version + - Pin exact versions (avoid ^ or ~ version ranges) + - Prevent duplicate dependencies in node_modules + - Keep dependencies up to date with compatible versions + +3. Common Issues to Avoid: + - Mixed versions of GalaChain packages + - Nested duplicates in node_modules + - Incompatible peer dependencies + - Unnecessary dependencies that bloat chaincode size + +4. Best Practices: + - Use package-lock.json or yarn.lock for deterministic installs + - Regularly audit dependencies for security issues + - Remove unused dependencies + - Test thoroughly after dependency updates + - Document dependency requirements + +Note: Mismatched versions between `@gala-chain/api` and `@gala-chain/chaincode` can cause runtime errors and unexpected behavior. Always ensure these packages are using the same version number. \ No newline at end of file diff --git a/docs/questions/handle-cross-chaincode-calls.md b/docs/questions/handle-cross-chaincode-calls.md new file mode 100644 index 0000000000..1e2d6444f1 --- /dev/null +++ b/docs/questions/handle-cross-chaincode-calls.md @@ -0,0 +1,32 @@ +### Question + + +What's the best way to handle cross-chaincode calls? + + +### Answer + + +It's important to understand that cross-chaincode calls are not supported in GalaChain. Here's what you need to know: + +1. Channel Concepts in Hyperledger Fabric: + - Channels in Hyperledger Fabric effectively function as separate chains + - Each channel maintains its own ledger and state database + - Chaincodes deployed on different channels cannot directly interact + +2. Cross-Channel Limitations: + - Read-only queries across channels are technically possible but limited + - Cross-channel write operations are not supported + - Each chaincode operates within its own channel context + +3. Cross-Chain Interactions: + - True cross-chain operations (e.g., with Ethereum or Solana) require specialized bridge solutions + - Token bridging between chains is possible but handled by dedicated infrastructure + - These are advanced use cases not typically encountered in regular GalaChain SDK development + +4. Best Practices: + - Design your application to operate within a single channel + - If cross-chain functionality is needed, work with the GalaChain team + - Consider using event-driven architectures for cross-channel communication + +Note: As a GalaChain SDK developer, focus on building functionality within your assigned channel. Cross-chain or cross-channel operations are specialized cases handled at the infrastructure level. \ No newline at end of file diff --git a/docs/questions/implement-custom-decorators-in-chaincode.md b/docs/questions/implement-custom-decorators-in-chaincode.md new file mode 100644 index 0000000000..6749783181 --- /dev/null +++ b/docs/questions/implement-custom-decorators-in-chaincode.md @@ -0,0 +1,73 @@ +### Question + + +How can I implement custom decorators in chaincode? + + +### Answer + + +Custom decorators in GalaChain chaincode can be implemented to add validation, logging, access control, or other cross-cutting concerns. Here's how to create and use them: + +1. Method Decorator Example: +```typescript +function ValidateOwnership() { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = async function (...args: any[]) { + const ctx = args[0] as GalaChainContext; + const params = args[1] as { tokenId: string }; + + // Perform ownership validation + const token = await getObjectByKey(ctx, Token, params.tokenId); + if (!token || token.owner !== ctx.callingUser.id) { + throw new Error('Caller does not own this token'); + } + + return originalMethod.apply(this, args); + }; + + return descriptor; + }; +} +``` + +2. Parameter Decorator Example: +```typescript +function ValidateParam(validation: (value: any) => boolean) { + return function (target: any, propertyKey: string, parameterIndex: number) { + const originalMethod = target[propertyKey]; + + target[propertyKey] = function (...args: any[]) { + const paramValue = args[parameterIndex]; + if (!validation(paramValue)) { + throw new Error(`Parameter validation failed at index ${parameterIndex}`); + } + return originalMethod.apply(this, args); + }; + }; +} +``` + +3. Usage Example: +```typescript +class GameContract extends Contract { + @Submit() + @ValidateOwnership() + async transferToken( + ctx: GalaChainContext, + @ValidateParam((id) => typeof id === 'string' && id.length > 0) + params: { tokenId: string, newOwner: string } + ): Promise { + // Transfer logic here + } +} +``` + +Best Practices: +1. Keep decorators focused on a single responsibility +2. Handle errors gracefully and provide clear error messages +3. Consider performance impact, especially for decorators that perform database operations +4. Document the decorator's purpose and requirements clearly +5. Use TypeScript's type system to ensure type safety \ No newline at end of file diff --git a/docs/questions/implement-role-based-access-control.md b/docs/questions/implement-role-based-access-control.md new file mode 100644 index 0000000000..1408ac3060 --- /dev/null +++ b/docs/questions/implement-role-based-access-control.md @@ -0,0 +1,77 @@ +### Question + + +How do I implement role-based access control (RBAC) in chaincode? + + +### Answer + + +Role-based access control in GalaChain is implemented through the platform's built-in RBAC system, available beginning with version 2.0+ of the SDK. Here's how to use it effectively: + +1. Available Role Checks: +```typescript +class GameContract extends Contract { + @Submit() + async adminOperation(ctx: GalaChainContext): Promise { + // Single role check + if (!ctx.callingUser.hasRole('ADMIN')) { + throw new Error('Admin access required'); + } + + // Multiple role check (any of these roles) + if (!ctx.callingUser.hasAnyRole(['ADMIN', 'MODERATOR'])) { + throw new Error('Admin or moderator access required'); + } + + // Organization membership check + if (!ctx.callingUser.isMemberOf('GameStudio')) { + throw new Error('Must be GameStudio member'); + } + } +} +``` + +2. Common Role Patterns: +```typescript +class TokenContract extends Contract { + // Use decorators to enforce role requirements + @Submit() + @RequireRole('MINTER') + async mintTokens( + ctx: GalaChainContext, + params: { amount: number } + ): Promise { + // Function body - role already verified by decorator + } + + // Combine multiple role checks for complex permissions + @Submit() + async transferTokens( + ctx: GalaChainContext, + params: { to: string, amount: number } + ): Promise { + const isAdmin = ctx.callingUser.hasRole('ADMIN'); + const isOwner = ctx.callingUser.id === params.to; + + if (!isAdmin && !isOwner) { + throw new Error('Insufficient permissions'); + } + } +} +``` + +3. Best Practices: + - Check roles at the beginning of functions + - Use descriptive role names that reflect responsibilities + - Consider implementing custom role decorators for common patterns + - Document role requirements in function comments + - Keep role assignments minimal (principle of least privilege) + +4. Important Notes: + - Roles are managed through the GalaChain admin interface + - Role assignments are immutable on chain + - Role checks are performed automatically by the platform + - Consider using role hierarchies for complex permissions + +Remember: RBAC is a critical security feature. Always verify that role checks are working as expected in your test environment before deploying to production. \ No newline at end of file diff --git a/docs/questions/monitor-chaincode-performance.md b/docs/questions/monitor-chaincode-performance.md new file mode 100644 index 0000000000..92c651afd7 --- /dev/null +++ b/docs/questions/monitor-chaincode-performance.md @@ -0,0 +1,73 @@ +### Question + + +How can I monitor chaincode performance? + + +### Answer + + +GalaChain provides several ways to monitor chaincode performance. Here's a comprehensive guide: + +1. Built-in Metrics: + - Transaction latency and throughput + - State database read/write operations + - Resource utilization (CPU, memory) + - Endorsement and validation times + - Analysis of block explorer data can yield historical performance metrics + +2. Logging Best Practices: +```typescript +class GameContract extends Contract { + @Submit() + async complexOperation( + ctx: GalaChainContext, + params: { itemIds: string[] } + ): Promise { + const startTime = Date.now(); + + // Log operation start with relevant context + ctx.logger.info('Starting complex operation', { + itemCount: params.itemIds.length, + caller: ctx.callingUser.id + }); + + try { + // Your operation logic here + for (const itemId of params.itemIds) { + await this.processItem(ctx, itemId); + } + + // Log successful completion with timing + ctx.logger.info('Complex operation completed', { + duration: Date.now() - startTime, + itemCount: params.itemIds.length + }); + } catch (error) { + // Log failures with details + ctx.logger.error('Complex operation failed', { + error: error.message, + duration: Date.now() - startTime, + failedAt: itemId + }); + throw error; + } + } +} +``` + +3. Performance Optimization Tips: + - Use batch operations when possible + - Minimize state database queries + - Implement proper indexing for frequently queried data + - Cache repeated lookups within a transaction + - Monitor and optimize complex range queries + +4. Monitoring Tools: + - Use Hyperledger Fabric's metrics service + - Monitor peer and orderer logs + - Set up alerts for performance thresholds + - Track endorsement policy evaluation times + - Monitor chaincode container resource usage + +Note: When investigating performance issues, always check for MVCC_READ_CONFLICTs and consider implementing batching or pagination for large operations. \ No newline at end of file diff --git a/docs/questions/secure-authentication.md b/docs/questions/secure-authentication.md new file mode 100644 index 0000000000..db98223a95 --- /dev/null +++ b/docs/questions/secure-authentication.md @@ -0,0 +1,57 @@ +### Question + + +How do I implement secure authentication in chaincode? + + +### Answer + + +GalaChain uses public/private key cryptography for authentication and authorization. Here are the key security aspects: + +1. Cryptographic Authentication: + - Users are authenticated using public/private key pairs + - Private keys must be securely stored and never exposed + - The system verifies digital signatures on DTOs (Data Transfer Objects) against the user's public key + - No personally identifiable information (PII), emails, or username/password combinations are stored on chain + +The platform provides built-in security infrastructure through the `ctx.callingUser` object, which contains the authenticated user's information. Here's how to use it: + +2. Implementation Example: +```typescript +class GameContract extends Contract { + @Submit() + async createItem( + ctx: GalaChainContext, + params: { itemId: string } + ): Promise { + // Check if user has admin role + if (!ctx.callingUser.hasRole('ADMIN')) { + throw new Error('Only admins can create items'); + } + + // Check organization membership + if (!ctx.callingUser.isMemberOf('GameStudio')) { + throw new Error('User must be part of GameStudio organization'); + } + + // Proceed with item creation + await this.createGameItem(ctx, params); + } +} +``` + +3. Security Best Practices: + - Always verify user roles and permissions before operations + - Use the built-in role-based access control (RBAC) + - Never implement custom authentication mechanisms + - Keep sensitive operations admin-only + - Log security-relevant events + +4. Important Considerations: + - Authentication is managed by the GalaChain Gateway + - User identities are based on X.509 certificates + - Role assignments are managed through the platform's admin interface + - Access control should be consistent across related operations + +Note: Focus on implementing proper authorization checks using the provided `ctx.callingUser` methods rather than creating custom authentication logic. \ No newline at end of file diff --git a/docs/questions/upgrading-chaincode-versions.md b/docs/questions/upgrading-chaincode-versions.md new file mode 100644 index 0000000000..80e7a4b983 --- /dev/null +++ b/docs/questions/upgrading-chaincode-versions.md @@ -0,0 +1,75 @@ +### Question + + +What's the best way to handle chaincode upgrades? + + +### Answer + + +Chaincode upgrades in GalaChain require careful planning and execution. Here's a comprehensive guide: + +1. Version Management: + - Use semantic versioning for chaincode versions + - Document all changes between versions + - Keep track of state schema changes + - Consider backward compatibility + +2. State Migration Example: +```typescript +class GameContract extends Contract { + @Submit() + async migrateState( + ctx: GalaChainContext, + params: { batchSize: number } + ): Promise { + // Verify admin permissions + if (!ctx.callingUser.hasRole('ADMIN')) { + throw new Error('Only admins can migrate state'); + } + + // Get items to migrate + const oldItems = await getStateRange(ctx, OldGameItem); + let migratedCount = 0; + + // Process in batches + for (const item of oldItems) { + if (migratedCount >= params.batchSize) { + break; // Continue in next transaction + } + + // Convert to new format + const newItem = new GameItem({ + ...item, + newField: computeNewField(item), + schemaVersion: '2.0' + }); + + // Store new version + await putChainObject(ctx, newItem); + migratedCount++; + } + + ctx.logger.info('State migration progress', { + migratedCount, + remaining: oldItems.length - migratedCount + }); + } +} +``` + +3. Upgrade Best Practices: + - Test upgrades thoroughly in development environment + - Plan for rollback scenarios + - Implement state validation checks + - Use progressive migrations for large state changes + - Keep old state readable during migration + +4. Deployment Considerations: + - Schedule upgrades during low-traffic periods + - Notify all network participants in advance + - Coordinate with all organization admins + - Verify endorsement policy requirements + - Monitor the upgrade process closely + +Note: Always maintain comprehensive documentation of your upgrade process, including state changes, new features, and any required actions from network participants. \ No newline at end of file From bcf531d6db02746cc265ec5f4cc7b8fc6f987126 Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Thu, 10 Apr 2025 16:54:18 -0700 Subject: [PATCH 05/12] docs: Q&A questions on swaps, large files, errors --- .../error-recovery-best-practices.md | 95 ++++++ .../fungible-vs-non-fungible-token-swaps.md | 102 +++++++ docs/questions/handle-expired-token-swaps.md | 227 +++++++++++++++ docs/questions/handle-timeouts.md | 237 +++++++++++++++ docs/questions/implement-a-token-swap.md | 125 ++++++++ docs/questions/implement-batch-token-swaps.md | 275 ++++++++++++++++++ docs/questions/large-file-storage.md | 91 ++++++ docs/questions/token-allowances-for-swaps.md | 250 ++++++++++++++++ docs/questions/transaction-hooks.md | 119 ++++++++ .../validate-token-swap-quantities.md | 117 ++++++++ 10 files changed, 1638 insertions(+) create mode 100644 docs/questions/error-recovery-best-practices.md create mode 100644 docs/questions/fungible-vs-non-fungible-token-swaps.md create mode 100644 docs/questions/handle-expired-token-swaps.md create mode 100644 docs/questions/handle-timeouts.md create mode 100644 docs/questions/implement-a-token-swap.md create mode 100644 docs/questions/implement-batch-token-swaps.md create mode 100644 docs/questions/large-file-storage.md create mode 100644 docs/questions/token-allowances-for-swaps.md create mode 100644 docs/questions/transaction-hooks.md create mode 100644 docs/questions/validate-token-swap-quantities.md diff --git a/docs/questions/error-recovery-best-practices.md b/docs/questions/error-recovery-best-practices.md new file mode 100644 index 0000000000..b2f7f7e2a5 --- /dev/null +++ b/docs/questions/error-recovery-best-practices.md @@ -0,0 +1,95 @@ +### Question + + +What are the best practices for error recovery in chaincode? + + +### Answer + + +Error recovery in GalaChain requires careful handling to maintain data consistency. Here's how to implement robust error handling: + +1. Custom Error Types: +```typescript +class ChainError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly details?: Record + ) { + super(message); + this.name = 'ChainError'; + } +} + +class ValidationError extends ChainError { + constructor(message: string, details?: Record) { + super(message, 'VALIDATION_ERROR', details); + } +} + +class InsufficientFundsError extends ChainError { + constructor(message: string, details?: Record) { + super(message, 'INSUFFICIENT_FUNDS', details); + } +} +``` + +2. Error Handling Pattern: +```typescript +class GameContract extends Contract { + @Submit() + async transferTokens( + ctx: GalaChainContext, + params: { amount: number, recipient: string } + ): Promise { + try { + // Validate inputs + if (params.amount <= 0) { + throw new ValidationError('Amount must be positive'); + } + + // Check balance + const balance = await getTokenBalance(ctx, ctx.callingUser); + if (balance.lt(params.amount)) { + throw new InsufficientFundsError('Insufficient balance', { + required: params.amount, + available: balance.toString() + }); + } + + // Perform transfer + await this.executeTransfer(ctx, params); + + } catch (error) { + // Log error with context + ctx.logger.error('Transfer failed', { + error: error.message, + code: error instanceof ChainError ? error.code : 'UNKNOWN_ERROR', + details: error instanceof ChainError ? error.details : undefined, + params + }); + + // Re-throw to trigger transaction rollback + throw error; + } + } +} +``` + +3. Recovery Strategies: + - Use atomic transactions + - Implement proper state validation + - Handle partial failures gracefully + - Provide clear error messages + - Log sufficient debug information + +4. Best Practices: + - Never catch errors silently + - Always log error context + - Use custom error types + - Include error codes + - Implement proper rollback logic + - Validate state before updates + +Note: Remember that chaincode transactions are atomic - they either complete successfully or roll back entirely. Use this to your advantage when designing error recovery strategies. \ No newline at end of file diff --git a/docs/questions/fungible-vs-non-fungible-token-swaps.md b/docs/questions/fungible-vs-non-fungible-token-swaps.md new file mode 100644 index 0000000000..d873a4bccc --- /dev/null +++ b/docs/questions/fungible-vs-non-fungible-token-swaps.md @@ -0,0 +1,102 @@ +### Question + + +What's the difference between fungible and non-fungible token swaps in GalaChain? + + +### Answer + + +Fungible and non-fungible token (NFT) swaps in GalaChain have distinct characteristics and implementation requirements. Here are the key differences: + +1. Token Identification: + - Fungible Tokens: + - Identified by token type/ID only + - Quantities are interchangeable + - Balance-based tracking + - Non-Fungible Tokens: + - Each token has a unique identifier + - Individual token tracking + - Specific instance transfers + +2. Implementation Example: +```typescript +class TokenSwapContract extends Contract { + // Fungible token swap + @Submit() + async swapFungibleTokens( + ctx: GalaChainContext, + params: { + offeredTokenId: string, + offeredAmount: number, + requestedTokenId: string, + requestedAmount: number + } + ): Promise { + // Balance-based validation + const balance = await getTokenBalance(ctx, ctx.callingUser, params.offeredTokenId); + if (balance.lt(params.offeredAmount)) { + throw new Error('Insufficient balance'); + } + // ... rest of swap logic + } + + // NFT swap + @Submit() + async swapNFTs( + ctx: GalaChainContext, + params: { + offeredNFTId: string, // Specific NFT instance + requestedNFTId: string // Specific NFT instance + } + ): Promise { + // Ownership validation + const nft = await getNFTById(ctx, params.offeredNFTId); + if (nft.owner !== ctx.callingUser) { + throw new Error('Not the owner of NFT'); + } + + // Validate NFT properties + await this.validateNFTAttributes(ctx, params.offeredNFTId, params.requestedNFTId); + // ... rest of swap logic + } + + // Hybrid swap (NFT for fungible tokens) + @Submit() + async swapNFTForTokens( + ctx: GalaChainContext, + params: { + nftId: string, + tokenId: string, + tokenAmount: number + } + ): Promise { + // Combined validation + await Promise.all([ + this.validateNFTOwnership(ctx, params.nftId), + this.validateTokenBalance(ctx, params.tokenId, params.tokenAmount) + ]); + // ... rest of swap logic + } +} +``` + +3. Key Differences in Implementation: + - Validation: + - Fungible: Balance and quantity checks + - NFT: Ownership and uniqueness verification + - Transfer Logic: + - Fungible: Amount-based transfers + - NFT: Instance-based transfers + - State Management: + - Fungible: Update balances + - NFT: Transfer ownership records + +4. Additional Considerations: + - NFT Metadata Handling + - Collection Validation + - Attribute Matching + - Transfer Restrictions + - Royalty Handling + +Note: The GalaChain SDK provides built-in support for both fungible and non-fungible token swaps through its swap contracts. Always use the SDK's implementation when possible as it handles many edge cases and security considerations. \ No newline at end of file diff --git a/docs/questions/handle-expired-token-swaps.md b/docs/questions/handle-expired-token-swaps.md new file mode 100644 index 0000000000..5f6afb8208 --- /dev/null +++ b/docs/questions/handle-expired-token-swaps.md @@ -0,0 +1,227 @@ +### Question + + +How do I handle expired token swaps? + + +### Answer + + +GalaChain provides a comprehensive approach to handle expired token swaps. Here's how to implement expiration handling: + +1. Swap Request with Expiration: +```typescript +export class SwapRequest extends ChainObject { + @ChainKey({ position: 0 }) + public readonly swapId: string; + + @ChainKey({ position: 1 }) + @IsUserAlias() + public readonly requester: UserAlias; + + public readonly fromToken: { + tokenId: string; + amount: number; + }; + + public readonly toToken: { + tokenId: string; + amount: number; + }; + + public readonly expiresAt: number; + public readonly status: 'pending' | 'completed' | 'expired' | 'cancelled'; + public readonly createdAt: number; + public readonly completedAt?: number; + + public hasExpired(): boolean { + return Date.now() >= this.expiresAt; + } +} + +@Submit() +async function createSwapRequest( + ctx: GalaChainContext, + params: { + fromToken: { + tokenId: string; + amount: number; + }; + toToken: { + tokenId: string; + amount: number; + }; + expiresInMs: number; + } +): Promise { + const swapId = generateUniqueId(); + const requester = ctx.callingUser; + + // Validate token ownership + const balance = await getBalance(ctx, { + tokenId: params.fromToken.tokenId, + owner: requester + }); + if (!balance || balance.amount < params.fromToken.amount) { + throw new Error('Insufficient balance for swap'); + } + + const request = new SwapRequest({ + swapId, + requester, + fromToken: params.fromToken, + toToken: params.toToken, + expiresAt: Date.now() + params.expiresInMs, + status: 'pending', + createdAt: Date.now() + }); + + await putChainObject(ctx, request); + return swapId; +} +``` + +2. Expiration Check and Cleanup: +```typescript +@Submit() +async function cleanupExpiredSwaps( + ctx: GalaChainContext +): Promise { + const requests = await getObjectsByPartialCompositeKey( + ctx, + SwapRequest.INDEX_KEY, + [], + SwapRequest + ); + + const expiredRequests = requests.filter(req => + req.status === 'pending' && req.hasExpired() + ); + + // Process expired requests sequentially + for (const req of expiredRequests) { + const expired = new SwapRequest({ + ...req, + status: 'expired' + }); + + // Release any locked tokens + await releaseTokenLock(ctx, { + tokenId: req.fromToken.tokenId, + owner: req.requester, + amount: req.fromToken.amount + }); + + await putChainObject(ctx, expired); + + // Emit expiration event + ctx.stub.setEvent('SwapExpired', { + swapId: req.swapId, + requester: req.requester, + expiresAt: req.expiresAt + }); + } +} +``` + +3. Expiration Validation: +```typescript +@Submit() +async function validateSwapRequest( + ctx: GalaChainContext, + params: { + swapId: string; + } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + SwapRequest.INDEX_KEY, + [params.swapId] + ); + + const request = await getObjectByKey(ctx, SwapRequest, key); + if (!request) { + throw new Error('Swap request not found'); + } + + if (request.status !== 'pending') { + throw new Error(`Swap is ${request.status}`); + } + + if (request.hasExpired()) { + // Auto-expire the request + const expired = new SwapRequest({ + ...request, + status: 'expired' + }); + await putChainObject(ctx, expired); + throw new Error('Swap request has expired'); + } +} +``` + +4. Expiration Monitoring: +```typescript +@Submit() +async function getExpiringSwaps( + ctx: GalaChainContext, + params: { + timeWindowMs: number; + } +): Promise { + const now = Date.now(); + const requests = await getObjectsByPartialCompositeKey( + ctx, + SwapRequest.INDEX_KEY, + [], + SwapRequest + ); + + return requests.filter(req => + req.status === 'pending' && + req.expiresAt > now && + req.expiresAt <= now + params.timeWindowMs + ); +} + +@Submit() +async function notifyExpiringSwaps( + ctx: GalaChainContext +): Promise { + const expiringSwaps = await getExpiringSwaps(ctx, { + timeWindowMs: 300000 // 5 minutes + }); + + // Process expiring swaps sequentially + for (const swap of expiringSwaps) { + ctx.stub.setEvent('SwapExpiringSoon', { + swapId: swap.swapId, + requester: swap.requester, + expiresAt: swap.expiresAt, + timeToExpiration: swap.expiresAt - Date.now() + }); + } +} +``` + +Best Practices: +- Set reasonable expiration times +- Implement auto-cleanup +- Monitor approaching expirations +- Release locked resources +- Emit expiration events + +Key Points: +- Validate expiration +- Clean up expired swaps +- Release token locks +- Track expiration status +- Notify stakeholders + +Expiration Tips: +- Use timestamp-based expiration +- Implement grace periods +- Monitor expiration patterns +- Archive expired swaps +- Handle partial completions + +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all, forEach, or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. This is especially important when handling expirations and cleanup operations where the order of execution must be consistent. \ No newline at end of file diff --git a/docs/questions/handle-timeouts.md b/docs/questions/handle-timeouts.md new file mode 100644 index 0000000000..d852104a42 --- /dev/null +++ b/docs/questions/handle-timeouts.md @@ -0,0 +1,237 @@ +### Question + + +How do I handle timeouts in chaincode? + + +### Answer + + +GalaChain provides several mechanisms for handling timeouts effectively. Here's how to implement timeout handling: + +1. Transaction Timeouts: +```typescript +@Submit() +async function longRunningOperation( + ctx: GalaChainContext, + params: { + operationId: string; + timeoutMs: number; + } +): Promise { + const startTime = Date.now(); + + // Check if operation already exists + const key = ChainObject.getCompositeKeyFromParts( + 'OPERATION', + [params.operationId] + ); + + const operation = await getObjectByKey(ctx, Operation, key); + if (operation) { + throw new Error('Operation already in progress'); + } + + // Create operation record + const newOperation = new Operation({ + id: params.operationId, + startTime: startTime, + timeoutAt: startTime + params.timeoutMs, + status: 'running' + }); + await putChainObject(ctx, newOperation); + + try { + // Perform operation with timeout check + while (someCondition) { + if (Date.now() > newOperation.timeoutAt) { + throw new TimeoutError('Operation timed out'); + } + await processNextBatch(); + } + + // Update status on success + const completed = new Operation({ + ...newOperation, + status: 'completed', + completedAt: Date.now() + }); + await putChainObject(ctx, completed); + } catch (error) { + // Update status on failure + const failed = new Operation({ + ...newOperation, + status: 'failed', + error: error.message + }); + await putChainObject(ctx, failed); + throw error; + } +} +``` + +2. Resource Lock Timeouts: +```typescript +export class TimedLock extends ChainObject { + @ChainKey({ position: 0 }) + public readonly resourceId: string; + + public readonly lockedBy?: string; + public readonly acquiredAt?: number; + public readonly timeoutMs: number; + + public isLocked(): boolean { + return this.lockedBy != null && + this.acquiredAt != null && + Date.now() < (this.acquiredAt + this.timeoutMs); + } + + public hasTimedOut(): boolean { + return this.lockedBy != null && + this.acquiredAt != null && + Date.now() >= (this.acquiredAt + this.timeoutMs); + } +} + +@Submit() +async function acquireResourceWithTimeout( + ctx: GalaChainContext, + params: { + resourceId: string; + userId: string; + timeoutMs: number; + } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + TimedLock.INDEX_KEY, + [params.resourceId] + ); + + const lock = await getObjectByKey(ctx, TimedLock, key); + if (lock) { + if (lock.isLocked() && lock.lockedBy !== params.userId) { + throw new Error('Resource is locked'); + } + if (lock.hasTimedOut()) { + // Auto-release timed out lock + await releaseResource(ctx, { resourceId: params.resourceId }); + } + } + + const newLock = new TimedLock({ + resourceId: params.resourceId, + lockedBy: params.userId, + acquiredAt: Date.now(), + timeoutMs: params.timeoutMs + }); + await putChainObject(ctx, newLock); +} +``` + +3. Async Operation Status: +```typescript +export class AsyncOperation extends ChainObject { + @ChainKey({ position: 0 }) + public readonly operationId: string; + + public readonly status: 'pending' | 'running' | 'completed' | 'failed'; + public readonly startTime: number; + public readonly timeoutMs: number; + public readonly result?: any; + public readonly error?: string; + + public hasTimedOut(): boolean { + return Date.now() >= (this.startTime + this.timeoutMs); + } +} + +@Submit() +async function checkOperationStatus( + ctx: GalaChainContext, + params: { + operationId: string; + } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + AsyncOperation.INDEX_KEY, + [params.operationId] + ); + + const operation = await getObjectByKey(ctx, AsyncOperation, key); + if (!operation) { + throw new Error('Operation not found'); + } + + if (operation.status === 'running' && operation.hasTimedOut()) { + // Update status for timed out operation + const timedOut = new AsyncOperation({ + ...operation, + status: 'failed', + error: 'Operation timed out' + }); + await putChainObject(ctx, timedOut); + return timedOut; + } + + return operation; +} +``` + +4. Cleanup of Timed Out Operations: +```typescript +@Submit() +async function cleanupTimedOutOperations( + ctx: GalaChainContext +): Promise { + const operations = await getObjectsByPartialCompositeKey( + ctx, + AsyncOperation.INDEX_KEY, + [], + AsyncOperation + ); + + const timedOutOps = operations.filter(op => + op.status === 'running' && op.hasTimedOut() + ); + + // Process operations sequentially + for (const op of timedOutOps) { + const failed = new AsyncOperation({ + ...op, + status: 'failed', + error: 'Operation timed out' + }); + await putChainObject(ctx, failed); + + // Emit timeout event + ctx.stub.setEvent('OperationTimeout', { + operationId: op.operationId, + startTime: op.startTime, + timeoutMs: op.timeoutMs + }); + } +} +``` + +Best Practices: +- Always set appropriate timeout values +- Implement cleanup mechanisms +- Use sequential operations (avoid Promise.all) +- Handle timeout events +- Monitor long-running operations + +Key Points: +- Use timestamp-based timeouts +- Implement auto-cleanup +- Handle partial completions +- Emit timeout events +- Log timeout occurrences + +Timeout Tips: +- Set reasonable timeouts +- Clean up timed out resources +- Monitor timeout patterns +- Implement retry logic +- Test timeout scenarios + +Note: The GalaChain SDK provides built-in timeout handling through its transaction context and chaincode interfaces. For most use cases, you should rely on these built-in mechanisms rather than implementing custom timeout logic. The examples above illustrate the concepts but consider using the SDK's timeout handling features in production. \ No newline at end of file diff --git a/docs/questions/implement-a-token-swap.md b/docs/questions/implement-a-token-swap.md new file mode 100644 index 0000000000..50d6baa685 --- /dev/null +++ b/docs/questions/implement-a-token-swap.md @@ -0,0 +1,125 @@ +### Question + + +How do I implement a token swap in GalaChain? + + +### Answer + + +Token swaps in GalaChain can be implemented using atomic transactions. + +Note that the public SDK provides a full-featured token swap implementation. See the full details in the chaincode/src/swaps directory. What belows below is a truncated, simplified example for illustrative purposes. Here's a very basic overview of how to create a secure swap mechanism: + +1. Swap Contract Implementation: +```typescript +class SwapContract extends Contract { + @Submit() + async createSwapOffer( + ctx: GalaChainContext, + params: { + offeredTokenId: string, + offeredAmount: number, + requestedTokenId: string, + requestedAmount: number, + expiryTime: number + } + ): Promise { + // Validate offer + const balance = await getTokenBalance(ctx, ctx.callingUser, params.offeredTokenId); + if (balance.lt(params.offeredAmount)) { + throw new Error('Insufficient balance for swap offer'); + } + + // Create and store swap offer + const offer = new SwapOffer({ + offerer: ctx.callingUser, + offeredTokenId: params.offeredTokenId, + offeredAmount: params.offeredAmount, + requestedTokenId: params.requestedTokenId, + requestedAmount: params.requestedAmount, + expiryTime: params.expiryTime, + status: 'ACTIVE' + }); + + await putChainObject(ctx, offer); + } + + @Submit() + async acceptSwapOffer( + ctx: GalaChainContext, + params: { offerId: string } + ): Promise { + // Get and validate offer + const offer = await getObjectByKey(ctx, SwapOffer, params.offerId); + if (offer.status !== 'ACTIVE' || offer.expiryTime < Date.now()) { + throw new Error('Offer is not active'); + } + + // Check accepter's balance + const accepterBalance = await getTokenBalance( + ctx, + ctx.callingUser, + offer.requestedTokenId + ); + if (accepterBalance.lt(offer.requestedAmount)) { + throw new Error('Insufficient balance to accept swap'); + } + + // Execute atomic swap + // Transfer offered tokens to accepter + await this.transferTokens(ctx, { + from: offer.offerer, + to: ctx.callingUser, + tokenId: offer.offeredTokenId, + amount: offer.offeredAmount + }); + + // Transfer requested tokens to offerer + await this.transferTokens(ctx, { + from: ctx.callingUser, + to: offer.offerer, + tokenId: offer.requestedTokenId, + amount: offer.requestedAmount + }); + + // Update offer status + offer.status = 'COMPLETED'; + await putChainObject(ctx, offer); + } + + @Submit() + async cancelSwapOffer( + ctx: GalaChainContext, + params: { offerId: string } + ): Promise { + const offer = await getObjectByKey(ctx, SwapOffer, params.offerId); + if (offer.offerer !== ctx.callingUser) { + throw new Error('Only offerer can cancel'); + } + if (offer.status !== 'ACTIVE') { + throw new Error('Offer is not active'); + } + + offer.status = 'CANCELLED'; + await putChainObject(ctx, offer); + } +} +``` + +2. Key Features: + - Atomic transactions ensure both transfers succeed or fail + - Balance validation before swap + - Offer expiry mechanism + - Cancellation support + - Status tracking + +3. Best Practices: + - Always validate balances + - Include expiry times + - Implement cancellation + - Use atomic operations + - Handle edge cases + - Log swap events + +Note: This implementation assumes the existence of appropriate token contracts and transfer methods. Always ensure proper access controls and validation are in place for production use. \ No newline at end of file diff --git a/docs/questions/implement-batch-token-swaps.md b/docs/questions/implement-batch-token-swaps.md new file mode 100644 index 0000000000..96167bb745 --- /dev/null +++ b/docs/questions/implement-batch-token-swaps.md @@ -0,0 +1,275 @@ +### Question + + +How can I implement batch token swaps in GalaChain? + + +### Answer + + +GalaChain provides efficient mechanisms for handling batch token swaps. Here's how to implement batch swap operations: + +1. Batch Swap Request Structure: +```typescript +export class BatchSwapRequest extends ChainObject { + @ChainKey({ position: 0 }) + public readonly batchId: string; + + @ChainKey({ position: 1 }) + @IsUserAlias() + public readonly requester: UserAlias; + + public readonly swaps: Array<{ + fromToken: { + tokenId: string; + amount: number; + }; + toToken: { + tokenId: string; + amount: number; + }; + recipient?: UserAlias; + }>; + + public readonly status: 'pending' | 'processing' | 'completed' | 'failed'; + public readonly createdAt: number; + public readonly completedAt?: number; + public readonly errors?: Array<{ swapIndex: number; error: string }>; +} + +@Submit() +async function createBatchSwapRequest( + ctx: GalaChainContext, + params: { + swaps: Array<{ + fromToken: { + tokenId: string; + amount: number; + }; + toToken: { + tokenId: string; + amount: number; + }; + recipient?: UserAlias; + }>; + } +): Promise { + const batchId = generateUniqueId(); + const requester = ctx.callingUser; + + // Validate all swaps first + for (let i = 0; i < params.swaps.length; i++) { + const swap = params.swaps[i]; + const balance = await getBalance(ctx, { + tokenId: swap.fromToken.tokenId, + owner: requester + }); + + if (!balance || balance.amount < swap.fromToken.amount) { + throw new Error(`Insufficient balance for swap ${i}`); + } + } + + const request = new BatchSwapRequest({ + batchId, + requester, + swaps: params.swaps, + status: 'pending', + createdAt: Date.now() + }); + + await putChainObject(ctx, request); + return batchId; +} +``` + +2. Batch Swap Execution: +```typescript +@Submit() +async function executeBatchSwap( + ctx: GalaChainContext, + params: { + batchId: string; + } +): Promise { + const key = ChainObject.getCompositeKeyFromParts( + BatchSwapRequest.INDEX_KEY, + [params.batchId] + ); + + const request = await getObjectByKey(ctx, BatchSwapRequest, key); + if (!request) { + throw new Error('Batch swap request not found'); + } + + if (request.status !== 'pending') { + throw new Error('Batch swap already processed'); + } + + // Update status to processing + const processing = new BatchSwapRequest({ + ...request, + status: 'processing' + }); + await putChainObject(ctx, processing); + + const errors: Array<{ swapIndex: number; error: string }> = []; + + // Process each swap + for (let i = 0; i < request.swaps.length; i++) { + const swap = request.swaps[i]; + try { + await executeSwap(ctx, { + fromToken: { + tokenId: swap.fromToken.tokenId, + owner: request.requester, + amount: swap.fromToken.amount + }, + toToken: { + tokenId: swap.toToken.tokenId, + owner: swap.recipient || request.requester, + amount: swap.toToken.amount + } + }); + } catch (error) { + errors.push({ + swapIndex: i, + error: error.message + }); + } + } + + // Update final status + const completed = new BatchSwapRequest({ + ...request, + status: errors.length === 0 ? 'completed' : 'failed', + completedAt: Date.now(), + errors: errors.length > 0 ? errors : undefined + }); + + await putChainObject(ctx, completed); + return completed; +} +``` + +3. Batch Swap Status Tracking: +```typescript +@Submit() +async function getBatchSwapStatus( + ctx: GalaChainContext, + params: { + batchId: string; + } +): Promise<{ + status: string; + completedSwaps: number; + failedSwaps: number; + errors?: Array<{ swapIndex: number; error: string }>; +}> { + const key = ChainObject.getCompositeKeyFromParts( + BatchSwapRequest.INDEX_KEY, + [params.batchId] + ); + + const request = await getObjectByKey(ctx, BatchSwapRequest, key); + if (!request) { + throw new Error('Batch swap request not found'); + } + + return { + status: request.status, + completedSwaps: request.status === 'completed' ? + request.swaps.length : + request.swaps.length - (request.errors?.length || 0), + failedSwaps: request.errors?.length || 0, + errors: request.errors + }; +} +``` + +4. Batch Swap Query and Cleanup: +```typescript +@Submit() +async function cleanupOldBatchSwaps( + ctx: GalaChainContext, + params: { + olderThan: number; // timestamp + } +): Promise { + const requests = await getObjectsByPartialCompositeKey( + ctx, + BatchSwapRequest.INDEX_KEY, + [], + BatchSwapRequest + ); + + const oldRequests = requests.filter(req => + req.createdAt < params.olderThan && + ['completed', 'failed'].includes(req.status) + ); + + // Archive old requests sequentially + for (const req of oldRequests) { + const archived = new ArchivedBatchSwap({ + ...req, + archivedAt: Date.now() + }); + await putChainObject(ctx, archived); + await deleteChainObject(ctx, req); + } +} + +@Submit() +async function queryBatchSwaps( + ctx: GalaChainContext, + params: { + requester?: UserAlias; + status?: 'pending' | 'processing' | 'completed' | 'failed'; + fromTimestamp?: number; + toTimestamp?: number; + } +): Promise { + const requests = await getObjectsByPartialCompositeKey( + ctx, + BatchSwapRequest.INDEX_KEY, + params.requester ? [params.requester] : [], + BatchSwapRequest + ); + + return requests.filter(req => { + if (params.status && req.status !== params.status) { + return false; + } + if (params.fromTimestamp && req.createdAt < params.fromTimestamp) { + return false; + } + if (params.toTimestamp && req.createdAt > params.toTimestamp) { + return false; + } + return true; + }); +} +``` + +Best Practices: +- Validate before execution +- Use sequential operations +- Handle partial failures +- Track swap status +- Implement cleanup + +Key Points: +- Batch validation +- Sequential processing +- Error handling +- Status tracking +- Data archival + +Batch Tips: +- Set reasonable batch sizes +- Monitor performance +- Handle timeouts +- Track metrics +- Clean up old data + +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. This is especially important in batch operations where the order of execution must be consistent. \ No newline at end of file diff --git a/docs/questions/large-file-storage.md b/docs/questions/large-file-storage.md new file mode 100644 index 0000000000..a5cda7da9f --- /dev/null +++ b/docs/questions/large-file-storage.md @@ -0,0 +1,91 @@ +### Question + + +What's the best way to handle large file storage in chaincode? + + +### Answer + + +Large files should not be stored directly in chaincode state. Instead, use off-chain storage and store references in chaincode. Here's how: + +1. Document Reference Pattern: +```typescript +class DocumentReference extends ChainObject { + @Exclude() + public static readonly INDEX_KEY = 'DOC_REF'; + + @ChainKey({ position: 0 }) + @IsUserAlias() + public readonly owner: UserAlias; + + @ChainKey({ position: 1 }) + @IsString() + public readonly documentHash: string; // Hash of the document contents + + @IsString() + public readonly storageReference: string; // URL or path to external storage + + @IsNumber() + public readonly timestamp: number; +} + +class DocumentContract extends Contract { + @Submit() + async storeDocumentReference( + ctx: GalaChainContext, + params: { + documentHash: string; + storageReference: string; + } + ): Promise { + const reference = new DocumentReference({ + owner: ctx.callingUser.id, + documentHash: params.documentHash, + storageReference: params.storageReference, + timestamp: Date.now() + }); + + await putChainObject(ctx, reference); + } +} +``` + +2. Best Practices: + - Store large files in external storage (IPFS, S3, etc.) + - Only store metadata and references on-chain + - Include file hashes for integrity verification + - Implement proper access control for external storage + - Consider file lifecycle management + +3. Document Verification: +```typescript +@Evaluate() +async function verifyDocument( + ctx: GalaChainContext, + params: { + documentHash: string; + content: string; + } +): Promise { + // Get document reference + const reference = await getObjectByKey( + ctx, + DocumentReference, + [ctx.callingUser.id, params.documentHash] + ); + + // Verify hash matches + const computedHash = hashContent(params.content); + return computedHash === reference.documentHash; +} +``` + +4. Important Considerations: + - Keep chaincode state size manageable + - Implement proper garbage collection + - Consider data privacy requirements + - Handle external storage availability + - Plan for storage cost management + +Note: When using external storage, ensure it meets your requirements for availability, durability, and access control. The chaincode should only store what's necessary for verification and access management. \ No newline at end of file diff --git a/docs/questions/token-allowances-for-swaps.md b/docs/questions/token-allowances-for-swaps.md new file mode 100644 index 0000000000..10320bd429 --- /dev/null +++ b/docs/questions/token-allowances-for-swaps.md @@ -0,0 +1,250 @@ +### Question + + +How do I handle token allowances in swaps? + + +### Answer + + +GalaChain provides robust mechanisms for handling token allowances in swaps. Here's how to implement and manage allowances in swap operations: + +1. Token Allowance Structure: +```typescript +export class SwapAllowance extends ChainObject { + @ChainKey({ position: 0 }) + public readonly tokenId: string; + + @ChainKey({ position: 1 }) + @IsUserAlias() + public readonly owner: UserAlias; + + @ChainKey({ position: 2 }) + @IsUserAlias() + public readonly spender: UserAlias; + + @Min(0) + public readonly amount: number; + + public readonly expiresAt?: number; + public readonly restrictions?: { + minPrice?: number; + maxPrice?: number; + allowedTokenTypes?: string[]; + }; +} + +@Submit() +async function grantSwapAllowance( + ctx: GalaChainContext, + params: { + tokenId: string; + spender: UserAlias; + amount: number; + expiresAt?: number; + restrictions?: { + minPrice?: number; + maxPrice?: number; + allowedTokenTypes?: string[]; + }; + } +): Promise { + const owner = ctx.callingUser; + + // Validate token ownership + const balance = await getBalance(ctx, { + tokenId: params.tokenId, + owner: owner + }); + if (!balance || balance.amount < params.amount) { + throw new Error('Insufficient balance for allowance'); + } + + const allowance = new SwapAllowance({ + tokenId: params.tokenId, + owner: owner, + spender: params.spender, + amount: params.amount, + expiresAt: params.expiresAt, + restrictions: params.restrictions + }); + + await putChainObject(ctx, allowance); +} +``` + +2. Swap Allowance Validation: +```typescript +@Submit() +async function validateSwapAllowance( + ctx: GalaChainContext, + params: { + tokenId: string; + owner: UserAlias; + amount: number; + proposedPrice: number; + targetTokenType: string; + } +): Promise { + const spender = ctx.callingUser; + + const key = ChainObject.getCompositeKeyFromParts( + SwapAllowance.INDEX_KEY, + [params.tokenId, params.owner, spender] + ); + + const allowance = await getObjectByKey(ctx, SwapAllowance, key); + if (!allowance) { + throw new Error('No allowance found'); + } + + // Check expiration + if (allowance.expiresAt && Date.now() > allowance.expiresAt) { + throw new Error('Allowance has expired'); + } + + // Check amount + if (allowance.amount < params.amount) { + throw new Error('Insufficient allowance'); + } + + // Check price restrictions + if (allowance.restrictions) { + if (allowance.restrictions.minPrice && + params.proposedPrice < allowance.restrictions.minPrice) { + throw new Error('Price below minimum allowed'); + } + if (allowance.restrictions.maxPrice && + params.proposedPrice > allowance.restrictions.maxPrice) { + throw new Error('Price above maximum allowed'); + } + if (allowance.restrictions.allowedTokenTypes && + !allowance.restrictions.allowedTokenTypes.includes(params.targetTokenType)) { + throw new Error('Token type not allowed for swap'); + } + } +} +``` + +3. Executing Swaps with Allowances: +```typescript +@Submit() +async function executeSwapWithAllowance( + ctx: GalaChainContext, + params: { + fromToken: { + tokenId: string; + owner: UserAlias; + amount: number; + }; + toToken: { + tokenId: string; + owner: UserAlias; + amount: number; + }; + } +): Promise { + const spender = ctx.callingUser; + + // Validate allowances + await validateSwapAllowance(ctx, { + tokenId: params.fromToken.tokenId, + owner: params.fromToken.owner, + amount: params.fromToken.amount, + proposedPrice: calculatePrice(params), + targetTokenType: await getTokenType(ctx, params.toToken.tokenId) + }); + + // Update allowance + const allowanceKey = ChainObject.getCompositeKeyFromParts( + SwapAllowance.INDEX_KEY, + [params.fromToken.tokenId, params.fromToken.owner, spender] + ); + + const allowance = await getObjectByKey(ctx, SwapAllowance, allowanceKey); + const updatedAllowance = new SwapAllowance({ + ...allowance, + amount: allowance.amount - params.fromToken.amount + }); + + // Execute swap sequentially + // First update the allowance + await putChainObject(ctx, updatedAllowance); + + // Then execute the transfers in a specific order + await transferToken(ctx, params.fromToken); + await transferToken(ctx, params.toToken); +} +``` + +4. Batch Allowance Operations: +```typescript +@Submit() +async function batchUpdateAllowances( + ctx: GalaChainContext, + params: { + operations: Array<{ + tokenId: string; + spender: UserAlias; + amount: number; + action: 'grant' | 'revoke' | 'modify'; + }>; + } +): Promise<{ + succeeded: string[]; + failed: Array<{ tokenId: string; error: string }>; + }> { + const results = { + succeeded: [] as string[], + failed: [] as Array<{ tokenId: string; error: string }> + }; + + for (const op of params.operations) { + try { + if (op.action === 'revoke') { + await revokeAllowance(ctx, { + tokenId: op.tokenId, + spender: op.spender + }); + } else { + await grantSwapAllowance(ctx, { + tokenId: op.tokenId, + spender: op.spender, + amount: op.amount + }); + } + results.succeeded.push(op.tokenId); + } catch (error) { + results.failed.push({ + tokenId: op.tokenId, + error: error.message + }); + } + } + + return results; +} +``` + +Best Practices: +- Validate allowances before swaps +- Implement expiration checks +- Use sequential operations +- Handle batch updates +- Monitor allowance usage + +Key Points: +- Check token ownership +- Validate restrictions +- Update atomically +- Handle expirations +- Track allowance history + +Allowance Tips: +- Set reasonable limits +- Implement price bounds +- Use token type restrictions +- Monitor usage patterns +- Clean up expired allowances + +Note: Always execute operations sequentially in a specific order to ensure deterministic behavior across multiple chaincode executions. Never use Promise.all or other parallel execution methods as they can lead to non-deterministic results. \ No newline at end of file diff --git a/docs/questions/transaction-hooks.md b/docs/questions/transaction-hooks.md new file mode 100644 index 0000000000..11e5a2a6fb --- /dev/null +++ b/docs/questions/transaction-hooks.md @@ -0,0 +1,119 @@ +### Question + + +How do I implement transaction hooks in chaincode? + + +### Answer + + +Transaction hooks in GalaChain can be implemented using decorators and middleware patterns. Here's how: + +1. Using Method Decorators: +```typescript +function ValidateOwnership() { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = async function (ctx: GalaChainContext, params: any) { + // Pre-execution hook + const item = await getObjectByKey(ctx, GameItem, params.itemId); + if (item.owner !== ctx.callingUser) { + throw new Error('Not the owner'); + } + + // Execute original method + const result = await originalMethod.call(this, ctx, params); + + // Post-execution hook + ctx.logger.info('Operation completed', { + method: propertyKey, + itemId: params.itemId + }); + + return result; + }; + + return descriptor; + }; +} + +class GameContract extends Contract { + @Submit() + @ValidateOwnership() + async transferItem( + ctx: GalaChainContext, + params: { itemId: string, newOwner: string } + ): Promise { + // Method implementation + } +} +``` + +2. Built-in Decorator Hooks: +```typescript +class GameContract extends Contract { + private async checkFees( + ctx: GalaChainContext, + params: { amount: number } + ): Promise { + const balance = await getTokenBalance(ctx, ctx.callingUser); + if (balance.lt(params.amount)) { + throw new Error('Insufficient funds for transaction fee'); + } + } + + @Submit({ + before: async (ctx, params) => this.checkFees(ctx, { amount: 10 }), + after: async (ctx) => ctx.logger.info('Transaction completed') + }) + async purchaseItem( + ctx: GalaChainContext, + params: { itemId: string } + ): Promise { + // Transaction logic here + } +} +``` + +3. Common Hook Use Cases: + - Transaction fee gates + - Input validation + - Access control checks + - Logging and monitoring + - State validation + - Event emission + +4. Transaction Context Extension: +```typescript +class GameContext extends GalaChainContext { + private _gameState: GameState | undefined; + + async getGameState(): Promise { + if (!this._gameState) { + this._gameState = await getObjectByKey( + this, + GameState, + 'CURRENT_STATE' + ); + } + return this._gameState; + } + + async validateGameState(): Promise { + const state = await this.getGameState(); + if (state.status !== 'ACTIVE') { + throw new Error('Game is not active'); + } + } +} +``` + +5. Best Practices: + - Keep hooks focused and single-purpose + - Handle errors appropriately + - Consider performance impact + - Document hook behavior + - Use typescript for type safety + +Note: The `@Submit`, `@Evaluate`, and other GalaChain decorators provide built-in `before` and `after` hook properties, which are the preferred way to implement transaction hooks. Custom decorators should only be used when the built-in hooks don't meet your needs. While hooks are powerful, overusing them can make code harder to understand and maintain. \ No newline at end of file diff --git a/docs/questions/validate-token-swap-quantities.md b/docs/questions/validate-token-swap-quantities.md new file mode 100644 index 0000000000..ecdcdec90a --- /dev/null +++ b/docs/questions/validate-token-swap-quantities.md @@ -0,0 +1,117 @@ +### Question + + +How can I validate token swap quantities in GalaChain? + + +### Answer + + +Token swap quantity validation in GalaChain requires careful consideration of decimal places, overflow protection, and exchange rates. Here's how to implement robust validation: + +1. Basic Quantity Validation: +```typescript +class SwapValidator { + static validateQuantities( + offeredAmount: BigNumber, + requestedAmount: BigNumber, + offeredDecimals: number, + requestedDecimals: number + ): void { + // Check for positive amounts + if (offeredAmount.lte(0) || requestedAmount.lte(0)) { + throw new ValidationError('Amounts must be positive'); + } + + // Check for decimal place overflow + const offeredDecimalStr = offeredAmount.toString(); + const requestedDecimalStr = requestedAmount.toString(); + const offeredDecimalPlaces = SwapValidator.getDecimalPlaces(offeredDecimalStr); + const requestedDecimalPlaces = SwapValidator.getDecimalPlaces(requestedDecimalStr); + + if (offeredDecimalPlaces > offeredDecimals) { + throw new ValidationError('Offered amount has too many decimal places'); + } + if (requestedDecimalPlaces > requestedDecimals) { + throw new ValidationError('Requested amount has too many decimal places'); + } + } + + private static getDecimalPlaces(numStr: string): number { + const parts = numStr.split('.'); + return parts.length > 1 ? parts[1].length : 0; + } +} +``` + +2. Exchange Rate Validation: +```typescript +class SwapContract extends Contract { + @Submit() + async validateAndCreateSwap( + ctx: GalaChainContext, + params: { + offeredTokenId: string, + offeredAmount: BigNumber, + requestedTokenId: string, + requestedAmount: BigNumber + } + ): Promise { + // Get token metadata + const offeredToken = await getTokenMetadata(ctx, params.offeredTokenId); + const requestedToken = await getTokenMetadata(ctx, params.requestedTokenId); + + // Validate quantities + SwapValidator.validateQuantities( + params.offeredAmount, + params.requestedAmount, + offeredToken.decimals, + requestedToken.decimals + ); + + // Calculate and validate exchange rate + const exchangeRate = this.calculateExchangeRate( + params.offeredAmount, + params.requestedAmount, + offeredToken.decimals, + requestedToken.decimals + ); + + // Check against allowed rate range + if (!this.isExchangeRateValid(exchangeRate)) { + throw new ValidationError('Exchange rate outside allowed range'); + } + + // Proceed with swap creation + await this.createSwap(ctx, params); + } + + private calculateExchangeRate( + offeredAmount: BigNumber, + requestedAmount: BigNumber, + offeredDecimals: number, + requestedDecimals: number + ): BigNumber { + const normalizedOffered = offeredAmount.times(10 ** offeredDecimals); + const normalizedRequested = requestedAmount.times(10 ** requestedDecimals); + return normalizedRequested.div(normalizedOffered); + } +} +``` + +3. Best Practices: + - Always use BigNumber for calculations + - Validate decimal places + - Check for positive amounts + - Consider token metadata + - Implement rate limits + - Handle rounding carefully + +4. Additional Validations: + - Maximum swap amounts + - Minimum swap amounts + - Daily volume limits + - User trading limits + - Token pair restrictions + +Note: Always consider the precision requirements of your specific use case. Some tokens may require different decimal place handling or have specific trading restrictions. \ No newline at end of file From c87de046ef2e6b82c087cf2f2faf2502a6c166a0 Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Fri, 11 Apr 2025 16:59:03 -0700 Subject: [PATCH 06/12] docs: Q&A content for token burns --- docs/questions/batching-token-burns.md | 76 ++++++++++++ docs/questions/burn-allowances.md | 116 ++++++++++++++++++ docs/questions/burn-counter-handling.md | 77 ++++++++++++ docs/questions/burn-errors-handling.md | 105 ++++++++++++++++ docs/questions/burn-history-tracking.md | 113 +++++++++++++++++ docs/questions/burn-quantities-validation.md | 87 ++++++++++++++ docs/questions/burn-tokens-usage.md | 97 +++++++++++++++ docs/questions/burning-NFTs-vs-fungibles.md | 120 +++++++++++++++++++ docs/questions/querying-burn-transactions.md | 71 +++++++++++ docs/questions/test-token-burns.md | 107 +++++++++++++++++ 10 files changed, 969 insertions(+) create mode 100644 docs/questions/batching-token-burns.md create mode 100644 docs/questions/burn-allowances.md create mode 100644 docs/questions/burn-counter-handling.md create mode 100644 docs/questions/burn-errors-handling.md create mode 100644 docs/questions/burn-history-tracking.md create mode 100644 docs/questions/burn-quantities-validation.md create mode 100644 docs/questions/burn-tokens-usage.md create mode 100644 docs/questions/burning-NFTs-vs-fungibles.md create mode 100644 docs/questions/querying-burn-transactions.md create mode 100644 docs/questions/test-token-burns.md diff --git a/docs/questions/batching-token-burns.md b/docs/questions/batching-token-burns.md new file mode 100644 index 0000000000..b9cd808f4c --- /dev/null +++ b/docs/questions/batching-token-burns.md @@ -0,0 +1,76 @@ +### Question + + +How can I implement batch token burns? + + +### Answer + + +GalaChain's built-in `burnTokens` function already supports batch operations through its array parameter. Here's how to use it: + +1. Using the Built-in Batch Support: +```typescript +import { burnTokens, BurnTokenQuantity, TokenInstanceKey } from '@gala-chain/chaincode'; +import { UserAlias } from '@gala-chain/api'; +import { BigNumber } from 'bignumber.js'; + +// Example batch burn in your chaincode +async function batchBurnTokens(ctx: GalaChainContext, params: { + owner: UserAlias, + burns: Array<{ + collection: string, + category: string, + type: string, + instance: string, + quantity: BigNumber + }> +}) { + // Create array of burn quantities + const toBurn: BurnTokenQuantity[] = params.burns.map(burn => ({ + tokenInstanceKey: new TokenInstanceKey({ + collection: burn.collection, + category: burn.category, + type: burn.type, + additionalKey: '', + instance: burn.instance + }), + quantity: burn.quantity + })); + + // Execute batch burn using the built-in function + const burns = await burnTokens(ctx, { + owner: params.owner, + toBurn, + preValidated: false + }); + + return burns; // Returns array of TokenBurn objects +} +``` + +2. Key Features: +- The `burnTokens` function accepts an array of `BurnTokenQuantity` objects +- Each `BurnTokenQuantity` specifies a token instance and amount to burn +- All burns in the batch are processed in a single transaction +- The function automatically handles: + * Allowance validation + * Balance checks + * Burn tracking + * Counter updates + +3. Best Practices: +- Group related burns together in a batch +- Keep batch sizes reasonable (avoid extremely large batches) +- Ensure all token instances exist before burning +- Verify sufficient balances for all burns +- Handle any validation errors appropriately + +Key Points: +- No custom implementation needed - use the built-in batch support +- All burns in a batch are atomic - they all succeed or all fail +- Batch burns are automatically tracked in burn history +- The SDK handles all validation and state updates +- Burns are processed sequentially for deterministic results + +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. \ No newline at end of file diff --git a/docs/questions/burn-allowances.md b/docs/questions/burn-allowances.md new file mode 100644 index 0000000000..c8bce73edb --- /dev/null +++ b/docs/questions/burn-allowances.md @@ -0,0 +1,116 @@ +### Question + + +How do I handle burn allowances in GalaChain? + + +### Answer + + +GalaChain provides built-in mechanisms for managing burn allowances through the `TokenAllowance` class and `grantAllowance` function. Here's how to use them: + +1. The `TokenAllowance` Class: +GalaChain provides a built-in `TokenAllowance` class in `@gala-chain/api` that tracks token allowances, including burn permissions: + +```typescript +import { TokenAllowance, AllowanceType } from '@gala-chain/api'; + +// TokenAllowance class structure +export class TokenAllowance extends ChainObject { + @ChainKey({ position: 0 }) + public grantedTo: UserAlias; // Who received the allowance + + @ChainKey({ position: 1 }) + public collection: string; // Token collection + + @ChainKey({ position: 2 }) + public category: string; // Token category + + @ChainKey({ position: 3 }) + public type: string; // Token type + + @ChainKey({ position: 4 }) + public additionalKey: string; // Additional identifier + + @ChainKey({ position: 5 }) + public instance: string; // Token instance + + public allowanceType: AllowanceType; // BURN, TRANSFER, etc. + public quantity: BigNumber; // Amount allowed + public uses: BigNumber; // Number of times allowance can be used + public expires: number; // When the allowance expires +} +``` + +2. Using the `grantAllowance` Function: +GalaChain provides a `grantAllowance` function in the chaincode library to create allowances: + +```typescript +import { grantAllowance, GrantAllowanceQuantity, AllowanceType } from '@gala-chain/chaincode'; + +// Example usage in your chaincode +async function createBurnAllowance(ctx: GalaChainContext, params: { + tokenInstance: TokenInstanceQueryKey, + grantedTo: UserAlias, + quantity: BigNumber, + uses?: BigNumber, + expires?: number +}) { + const allowances = await grantAllowance(ctx, { + tokenInstance: params.tokenInstance, + allowanceType: AllowanceType.BURN, + quantities: [{ + grantedTo: params.grantedTo, + quantity: params.quantity + }], + uses: params.uses ?? new BigNumber(1), + expires: params.expires ?? inverseTime(Date.now() + 24 * 60 * 60 * 1000) // 24 hours + }); + return allowances; +} +``` + +3. Contract Implementation Example: +The GalaChain token contract provides built-in methods for managing burn allowances: + +```typescript +import { GrantAllowanceDto, TokenAllowance, AllowanceType } from '@gala-chain/api'; + +@Submit({ + in: GrantAllowanceDto, + out: { arrayOf: TokenAllowance } +}) +public async GrantBurnAllowance(ctx: GalaChainContext, dto: GrantAllowanceDto): Promise { + return grantAllowance(ctx, { + tokenInstance: dto.tokenInstance, + allowanceType: AllowanceType.BURN, + quantities: dto.quantities, + uses: dto.uses, + expires: dto.expires + }); +} + +``` + +Best Practices: +- Use the built-in `TokenAllowance` class for consistent allowance tracking +- Leverage the `grantAllowance` function for standardized allowance creation +- Set appropriate expiration times +- Specify the number of uses allowed +- Track allowance usage + +Key Points: +- Allowances are managed through the standard `TokenAllowance` class +- The `AllowanceType.BURN` type specifically enables burn permissions +- Allowances can be time-limited through the `expires` field +- Usage can be limited through the `uses` field +- Allowances are tracked and validated automatically + +Allowance Tips: +- Always set an expiration time +- Consider limiting the number of uses +- Use the built-in validation +- Monitor allowance events +- Keep allowance records + +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. \ No newline at end of file diff --git a/docs/questions/burn-counter-handling.md b/docs/questions/burn-counter-handling.md new file mode 100644 index 0000000000..e49c13815a --- /dev/null +++ b/docs/questions/burn-counter-handling.md @@ -0,0 +1,77 @@ +### Question + +How do I handle burn counters in GalaChain? + +### Answer + +GalaChain automatically manages burn counters through the `TokenBurnCounter` system. Here's how to work with burn counters: + +#### 1. Understanding TokenBurnCounter + +The `TokenBurnCounter` is a ranged chain object that tracks burn quantities for tokens. It includes: +- Token instance properties (collection, category, type, additionalKey) +- Burn metadata (burnedBy, timeKey, quantity) +- Total known burns count + +#### 2. Automatic Counter Management + +When using the `burnTokens` function, GalaChain automatically: +1. Creates or updates the `TokenBurnCounter` for each burn operation +2. Maintains the total known burns count +3. Ensures transaction atomicity + +```typescript +// Example burn operation - counters are handled automatically +const dto = await createValidSubmitDTO(BurnTokensDto, { + tokenInstances: [{ tokenInstanceKey, quantity: burnQty }] +}).signed(privateKey); + +const response = await contract.BurnTokens(ctx, dto); +``` + +#### 3. Querying Burn Counts + +To fetch the total known burn count for a token: + +```typescript +const params: FetchBurnCounterParams = { + collection: "my-collection", + category: "currency", + type: "gold", + additionalKey: "season1" +}; + +const totalBurns = await fetchKnownBurnCount(ctx, params); +``` + +#### 4. Counter Properties + +Each `TokenBurnCounter` includes: +- `quantity`: The amount burned in this specific counter +- `totalKnownBurnsCount`: Running total of all burns for this token +- `referenceId`: Unique identifier for the burn counter +- `timeKey`: Timestamp-based key for ordering + +#### Key Points + +1. **Automatic Updates**: + - Counters are automatically created and updated + - No manual counter management is needed + - Updates are atomic with burn operations + +2. **Time-Based Tracking**: + - Counters use time-based keys for ordering + - Historical burn data is preserved + - Queries can be filtered by time periods + +3. **Deterministic Behavior**: + - Counter updates follow GalaChain's deterministic execution model + - Sequential processing ensures consistency + - Atomic updates prevent race conditions + +4. **Query Considerations**: + - Use specific parameters to limit query scope + - Consider pagination for large result sets + - Filter by time periods when needed + +Remember that burn counters are managed automatically by the SDK - there's no need to manually create or update them when performing burn operations. \ No newline at end of file diff --git a/docs/questions/burn-errors-handling.md b/docs/questions/burn-errors-handling.md new file mode 100644 index 0000000000..058585ee33 --- /dev/null +++ b/docs/questions/burn-errors-handling.md @@ -0,0 +1,105 @@ +### Question + + +What's the best way to handle burn errors in GalaChain? + + +### Answer + + +GalaChain provides a set of built-in error types for handling burn-related errors. Here's how to use them effectively: + +1. Built-in Error Types: +```typescript +import { + InsufficientBalanceError, + NftMultipleBurnNotAllowedError, + BurnTokensFailedError, + InsufficientBurnAllowanceError, + UseAllowancesFailedError +} from '@gala-chain/chaincode'; + +// Example error handling in your chaincode +async function handleBurnOperation(ctx: GalaChainContext, params: { + owner: UserAlias, + toBurn: BurnTokenQuantity[] +}) { + try { + const burns = await burnTokens(ctx, { + owner: params.owner, + toBurn: params.toBurn, + preValidated: false + }); + return burns; + + } catch (error) { + // Handle specific burn errors + if (error instanceof InsufficientBalanceError) { + // User doesn't have enough tokens to burn + const { owner, spendableQuantity, quantity, tokenInstanceKey } = error.payload; + // Handle insufficient balance... + + } else if (error instanceof NftMultipleBurnNotAllowedError) { + // Attempted to burn multiple instances of an NFT + const { tokenInstanceKey } = error.payload; + // Handle NFT burn error... + + } else if (error instanceof InsufficientBurnAllowanceError) { + // User doesn't have sufficient burn allowance + const { user, allowedQuantity, quantity, tokenInstanceKey } = error.payload; + // Handle insufficient allowance... + + } else if (error instanceof UseAllowancesFailedError) { + // Failed to use burn allowances + const { quantity, tokenInstanceKey, owner } = error.payload; + // Handle allowance usage error... + + } else if (error instanceof BurnTokensFailedError) { + // General burn operation failure + // Handle general burn error... + + } else { + // Handle unexpected errors + throw error; + } + } +} +``` + +2. Error Types and Their Use Cases: + +- `InsufficientBalanceError`: + * When user tries to burn more tokens than they own + * Includes details about available and requested amounts + +- `NftMultipleBurnNotAllowedError`: + * When attempting to burn multiple instances of an NFT + * NFTs must be burned one at a time + +- `InsufficientBurnAllowanceError`: + * When user lacks required burn allowance + * Includes details about allowed and requested amounts + +- `UseAllowancesFailedError`: + * When burn allowance usage fails + * Could be due to expired or invalid allowances + +- `BurnTokensFailedError`: + * General burn operation failure + * Includes detailed error message and context + +3. Best Practices: +- Always use try-catch blocks around burn operations +- Handle each error type specifically +- Provide clear error messages to users +- Log errors appropriately +- Maintain transaction atomicity + +Key Points: +- Use the SDK's built-in error types +- Each error includes relevant context in its payload +- Errors are strongly typed for better handling +- Error messages are standardized +- Error handling preserves transaction atomicity + +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. \ No newline at end of file diff --git a/docs/questions/burn-history-tracking.md b/docs/questions/burn-history-tracking.md new file mode 100644 index 0000000000..21e24a2572 --- /dev/null +++ b/docs/questions/burn-history-tracking.md @@ -0,0 +1,113 @@ +### Question + + +How do I track burn history in GalaChain? + + +### Answer + + +GalaChain automatically tracks burn history through the built-in `TokenBurn` and `TokenBurnCounter` objects that are written to the chain during burn operations. Here's how to use them: + +1. Token Burn Records: +When using the `burnTokens` function, each burn operation creates a `TokenBurn` record: + +```typescript +import { TokenBurn } from '@gala-chain/api'; + +// TokenBurn objects are automatically created and contain: +export class TokenBurn extends ChainObject { + @ChainKey({ position: 0 }) + public burnedBy: UserAlias; // Who performed the burn + + @ChainKey({ position: 1 }) + public collection: string; // Token collection + + @ChainKey({ position: 2 }) + public category: string; // Token category + + @ChainKey({ position: 3 }) + public type: string; // Token type + + @ChainKey({ position: 4 }) + public additionalKey: string; // Additional identifier + + @ChainKey({ position: 5 }) + public instance: string; // Token instance + + public quantity: BigNumber; // Amount burned +} +``` + +2. Burn Counter Tracking: +The SDK also maintains `TokenBurnCounter` objects to track burn statistics: + +```typescript +import { TokenBurnCounter } from '@gala-chain/api'; + +// TokenBurnCounter tracks burn statistics: +export class TokenBurnCounter extends RangedChainObject { + @ChainKey({ position: 0 }) + public collection: string; // Token collection + + @ChainKey({ position: 1 }) + public category: string; // Token category + + @ChainKey({ position: 2 }) + public type: string; // Token type + + @ChainKey({ position: 3 }) + public additionalKey: string; // Additional identifier + + @ChainKey({ position: 4 }) + public timeKey: string; // Time period for the counter + + @ChainKey({ position: 5 }) + public burnedBy: UserAlias; // Who performed the burns + + public quantity: BigNumber; // Total amount burned in this period +} +``` + +3. Viewing Burn History: +You can track burn history in several ways: + +a) Using the Block Explorer: +- Navigate to the block explorer for your GalaChain network +- Search for `TokenBurn` objects using the following filters: + * Collection + * Category + * Type + * Instance (for NFTs) + * BurnedBy (user who performed the burn) +- View burn details including: + * Quantity burned + * Timestamp + * Transaction ID + +b) Using TokenBurnCounter Statistics: +- Search for `TokenBurnCounter` objects to view aggregated statistics +- Filter by: + * Collection/Category/Type + * Time period + * Burning user +- View statistics including: + * Total quantity burned + * Burn frequency + * Time-based trends + +Best Practices: +- Regularly monitor burn activity through the block explorer +- Use the time period filters to analyze burn patterns +- Track both individual burns and aggregated statistics +- Keep records of significant burn events +- Monitor burn rates for different token types + +Key Points: +- All burns are automatically tracked by the SDK +- No custom implementation needed - use the block explorer +- Both individual burns and statistics are available +- Data is permanently stored on chain +- Historical data can be analyzed at any time + +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. \ No newline at end of file diff --git a/docs/questions/burn-quantities-validation.md b/docs/questions/burn-quantities-validation.md new file mode 100644 index 0000000000..b88763a587 --- /dev/null +++ b/docs/questions/burn-quantities-validation.md @@ -0,0 +1,87 @@ +### Question + + +How do I validate burn quantities in GalaChain? + + +### Answer + + +GalaChain's SDK provides built-in validation for burn quantities. Here's how it works: + +1. Automatic Validations: +```typescript +import { burnTokens, BurnTokenQuantity, TokenInstanceKey } from '@gala-chain/chaincode'; +import { UserAlias } from '@gala-chain/api'; +import { BigNumber } from 'bignumber.js'; + +// The burnTokens function automatically validates: +async function burnTokens(ctx: GalaChainContext, params: { + owner: UserAlias, + toBurn: BurnTokenQuantity[] +}) { + // Validation happens automatically: + // - Balance checks + // - Decimal limits + // - NFT restrictions + // - Allowance verification + // - Token existence + + const burns = await burnTokens(ctx, { + owner: params.owner, + toBurn: params.toBurn, + preValidated: false // Set to true to skip allowance checks + }); + + return burns; +} +``` + +2. Built-in Validation Rules: + +- Balance Validation: + * Checks if owner has sufficient balance + * Prevents burning more than available + * Validates across batch operations + +- NFT Validation: + * Ensures NFTs are burned one at a time + * Prevents multiple NFT burns in single request + * Validates NFT ownership + +- Decimal Validation: + * Enforces token decimal limits + * Prevents burns with invalid decimals + * Respects token class configuration + +- Allowance Validation: + * Verifies burn allowances + * Checks allowance quantities + * Validates allowance expiration + +3. Error Types: + +- `InsufficientBalanceError`: + * When burn amount exceeds balance + * Includes available and requested amounts + +- `NftMultipleBurnNotAllowedError`: + * When attempting multiple NFT burns + * Enforces single NFT burn rule + +- `InvalidDecimalError`: + * When burn amount has invalid decimals + * Based on token class configuration + +- `InsufficientBurnAllowanceError`: + * When burn allowance is insufficient + * Includes allowance details + +Key Points: +- No manual validation needed +- SDK handles all validation rules +- Strong error typing for handling +- Atomic validation in transactions +- Built-in security checks + +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. \ No newline at end of file diff --git a/docs/questions/burn-tokens-usage.md b/docs/questions/burn-tokens-usage.md new file mode 100644 index 0000000000..d120b77daf --- /dev/null +++ b/docs/questions/burn-tokens-usage.md @@ -0,0 +1,97 @@ +### Question + + +How do I implement token burns in GalaChain? + + +### Answer + + +GalaChain provides built-in mechanisms for burning tokens through the `TokenBurn` class and `burnTokens` function. Here's how to use them: + +1. The `TokenBurn` Class: +GalaChain provides a built-in `TokenBurn` class in `@gala-chain/api` that tracks token burns: + +```typescript +import { TokenBurn } from '@gala-chain/api'; + +// TokenBurn class structure +export class TokenBurn extends ChainObject { + @ChainKey({ position: 0 }) + public burnedBy: UserAlias; // Who performed the burn + + @ChainKey({ position: 1 }) + public collection: string; // Token collection + + @ChainKey({ position: 2 }) + public category: string; // Token category + + @ChainKey({ position: 3 }) + public type: string; // Token type + + @ChainKey({ position: 4 }) + public additionalKey: string; // Additional identifier + + @ChainKey({ position: 5 }) + public instance: string; // Token instance + + public quantity: BigNumber; // Amount burned +} +``` + +2. Using the `burnTokens` Function: +GalaChain provides a `burnTokens` function in the chaincode library that handles token burns: + +```typescript +import { burnTokens, BurnTokenQuantity } from '@gala-chain/chaincode'; + +// Example usage in your chaincode +async function burnMyTokens(ctx: GalaChainContext, owner: UserAlias, toBurn: BurnTokenQuantity[]) { + const burns = await burnTokens(ctx, { + owner, // Owner of the tokens to burn + toBurn, // Array of tokens to burn + preValidated: false // Set to true if you've already validated allowances + }); + return burns; // Returns array of TokenBurn objects +} +``` + +3. Contract Implementation Example: +The GalaChain token contract provides a built-in `BurnTokens` method that you can use or reference: + +```typescript +import { BurnTokensDto, TokenBurn } from '@gala-chain/api'; + +@Submit({ + in: BurnTokensDto, + out: { arrayOf: TokenBurn } +}) +public async BurnTokens(ctx: GalaChainContext, dto: BurnTokensDto): Promise { + return burnTokens(ctx, { + owner: await resolveUserAlias(ctx, dto.owner ?? ctx.callingUser), + toBurn: dto.toBurn, + preValidated: false + }); +} +``` + +Best Practices: +- Always validate token ownership before burning +- Use sequential operations for multiple burns +- Handle errors gracefully +- Track burn history +- Emit burn events + +Key Points: +- Burns are permanent and irreversible +- Burns require proper authorization +- Burns update token balances +- Burns create audit records +- Burns may require allowances + +Burn Tips: +- Verify token ownership +- Check burn allowances +- Use proper error handling +- Monitor burn patterns +- Keep burn records diff --git a/docs/questions/burning-NFTs-vs-fungibles.md b/docs/questions/burning-NFTs-vs-fungibles.md new file mode 100644 index 0000000000..51c4b3e327 --- /dev/null +++ b/docs/questions/burning-NFTs-vs-fungibles.md @@ -0,0 +1,120 @@ +### Question + + +What's the difference between burning NFTs and fungible tokens? + + +### Answer + + +GalaChain provides built-in mechanisms for burning both NFTs and fungible tokens through the same core types and functions, but with some key differences in how they're handled: + +1. Token Burn Structure: +Both NFTs and fungible tokens use the `TokenBurn` class from `@gala-chain/api`: + +```typescript +import { TokenBurn } from '@gala-chain/api'; + +// TokenBurn class structure - used for both NFTs and fungibles +export class TokenBurn extends ChainObject { + @ChainKey({ position: 0 }) + public burnedBy: UserAlias; // Who performed the burn + + @ChainKey({ position: 1 }) + public collection: string; // Token collection + + @ChainKey({ position: 2 }) + public category: string; // Token category + + @ChainKey({ position: 3 }) + public type: string; // Token type + + @ChainKey({ position: 4 }) + public additionalKey: string; // Additional identifier + + @ChainKey({ position: 5 }) + public instance: string; // Token instance + + public quantity: BigNumber; // Amount burned +} +``` + +2. Burn Quantity Specification: +When burning tokens, you specify the burn details using `BurnTokenQuantity`: + +```typescript +import { BurnTokenQuantity, TokenInstanceKey } from '@gala-chain/api'; + +// For NFTs - quantity is always 1 +const nftBurn: BurnTokenQuantity = { + tokenInstanceKey: new TokenInstanceKey({ + collection: 'my-nfts', + category: 'characters', + type: 'hero', + additionalKey: '', + instance: '123' // Specific NFT instance + }), + quantity: new BigNumber(1) +}; + +// For fungible tokens - quantity can be any positive number +const fungibleBurn: BurnTokenQuantity = { + tokenInstanceKey: new TokenInstanceKey({ + collection: 'game-currency', + category: 'coins', + type: 'gold', + additionalKey: '', + instance: '0' // Usually '0' for fungibles + }), + quantity: new BigNumber(1000) +}; +``` + +3. Using the burnTokens Function: +The same `burnTokens` function is used for both types: + +```typescript +import { burnTokens } from '@gala-chain/chaincode'; + +// Example usage in your chaincode +async function burnTokens(ctx: GalaChainContext, params: { + owner: UserAlias, + toBurn: BurnTokenQuantity[] +}) { + const burns = await burnTokens(ctx, { + owner: params.owner, + toBurn: params.toBurn, + preValidated: false + }); + return burns; // Returns array of TokenBurn objects +} +``` + +Key Differences: +1. Instance Handling: + - NFTs: Each token has a unique instance ID that must be specified + - Fungibles: Use instance '0' as they are interchangeable + +2. Quantity Handling: + - NFTs: Always burn quantity of 1 (the entire token) + - Fungibles: Can burn any positive quantity up to the balance + +3. Balance Impact: + - NFTs: Balance goes to 0 and token instance is removed + - Fungibles: Balance is reduced by burn amount + +Best Practices: +- Use the built-in `TokenBurn` class for consistent burn tracking +- Leverage the `burnTokens` function for standardized burn operations +- Always validate token ownership before burning +- Set appropriate burn quantities (1 for NFTs) +- Track burn events for auditing + +Key Points: +- Both NFTs and fungibles use the same core types and functions +- The main difference is in how instances and quantities are handled +- The `TokenBurn` class tracks all burns consistently +- The `burnTokens` function handles validation automatically +- Burns are permanent and irreversible + +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. \ No newline at end of file diff --git a/docs/questions/querying-burn-transactions.md b/docs/questions/querying-burn-transactions.md new file mode 100644 index 0000000000..c67b530e4e --- /dev/null +++ b/docs/questions/querying-burn-transactions.md @@ -0,0 +1,71 @@ +### Question + +How can I query burn transactions in GalaChain? + +### Answer + +GalaChain provides built-in functionality to query burn transactions through the `fetchBurns` function. This allows you to retrieve `TokenBurn` entries from the chain's World State using various filtering parameters. + +#### Basic Usage + +```typescript +import { FetchBurnsDto, TokenBurn } from '@gala-chain/api'; + +// Create a fetch burns request +const dto = await createValidDTO(FetchBurnsDto, { + burnedBy: 'user123' // Required: the identity key of the user who performed the burns +}).signed(privateKey); + +// Query burns through the contract +const response = await contract.FetchBurns(ctx, dto); +``` + +#### Filtering Options + +You can filter burn transactions using these parameters: + +- `burnedBy` (required): Identity key of the user who performed the burn +- `collection`: Filter by token collection +- `category`: Filter by token category +- `type`: Filter by token type +- `additionalKey`: Filter by additional key +- `instance`: Filter by specific token instance +- `created`: Filter by creation timestamp + +#### Example with Filters + +```typescript +const dto = await createValidDTO(FetchBurnsDto, { + burnedBy: 'user123', + collection: 'MyCollection', + category: 'Items', + type: 'Weapon' +}).signed(privateKey); +``` + +#### Important Notes + +1. Results are sorted by ascending creation date (oldest first) +2. The query uses the `TokenBurn` class's `@ChainKeys` for filtering, which are ordered and cannot be skipped +3. Broad queries with many results could exceed the configured maximum and throw an error +4. Use more specific filters to limit the result set for better performance + +#### Example Response + +```typescript +// Successful response will contain an array of TokenBurn objects +GalaChainResponse.Success([ + { + burnedBy: 'user123', + collection: 'MyCollection', + category: 'Items', + type: 'Weapon', + instance: '1', + created: 1234567890, + quantity: new BigNumber('1') + }, + // ... more burn records +]); +``` + +For tracking total burns over time, you can also use the `TokenBurnCounter` class which maintains aggregated burn statistics. The burn counters are automatically updated when tokens are burned using the SDK's `burnTokens` function. \ No newline at end of file diff --git a/docs/questions/test-token-burns.md b/docs/questions/test-token-burns.md new file mode 100644 index 0000000000..35bfd3e4fd --- /dev/null +++ b/docs/questions/test-token-burns.md @@ -0,0 +1,107 @@ +### Question + +What's the best way to test token burns? + +### Answer + +GalaChain provides a comprehensive testing framework that makes it easy to test token burn operations. Here's how to effectively test token burns: + +#### 1. Using the Test Fixture + +The SDK provides a `fixture` helper for setting up test environments: + +```typescript +import { fixture } from '@gala-chain/test'; +import GalaChainTokenContract from './GalaChainTokenContract'; + +const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.testUser1) + .savedState(tokenClass, tokenInstance, tokenBalance) + .savedRangeState([]); +``` + +#### 2. Testing Basic Burns + +```typescript +// Given +const tokenInstance = currency.tokenInstance(); +const tokenInstanceKey = currency.tokenInstanceKey(); +const burnQty = new BigNumber('1'); + +// Create burn request +const dto = await createValidSubmitDTO(BurnTokensDto, { + tokenInstances: [{ tokenInstanceKey, quantity: burnQty }] +}).signed(privateKey); + +// When +const response = await contract.BurnTokens(ctx, dto); + +// Then +expect(response).toEqual(GalaChainResponse.Success([expectedTokenBurn])); +expect(getWrites()).toEqual( + writesMap( + plainToInstance(TokenBalance, { ...tokenBalance, quantity: new BigNumber('999') }), + tokenBurn, + tokenBurnCounter + ) +); +``` + +#### 3. Testing Error Cases + +```typescript +// Test insufficient balance +const response = await contract.BurnTokens(ctx, dto); +expect(response).toEqual( + GalaChainResponse.Error( + new InsufficientBurnAllowanceError( + users.testUser2.identityKey, + new BigNumber('0'), + burnQty, + tokenInstanceKey, + users.testUser1.identityKey + ) + ) +); +``` + +#### 4. Testing Batch Burns + +```typescript +const burn1 = { tokenInstanceKey, quantity: new BigNumber('1') }; +const burn2 = { tokenInstanceKey, quantity: new BigNumber('2') }; + +const dto = await createValidSubmitDTO(BurnTokensDto, { + tokenInstances: [burn1, burn2] +}).signed(privateKey); + +const response = await contract.BurnTokens(ctx, dto); +// Verify combined burn quantity +expect(tokenBurn.quantity).toEqual(new BigNumber('3')); +``` + +#### Key Testing Points + +1. **State Setup**: + - Initialize token class, instance, and balance + - Set up proper allowances if testing permissioned burns + - Configure initial token balances + +2. **Validation Testing**: + - Test decimal limits + - Test insufficient balances + - Test missing or invalid allowances + - Test NFT burn restrictions + +3. **State Changes**: + - Verify token balance updates + - Check TokenBurn record creation + - Validate TokenBurnCounter updates + - Ensure transaction atomicity + +4. **Error Handling**: + - Test all relevant error conditions + - Verify error messages and types + - Ensure no state changes on errors + +Remember to use the SDK's built-in test utilities (`fixture`, `writesMap`) and helper functions to make your tests more readable and maintainable. \ No newline at end of file From 9bcfea3d777c1e320794fcfab2f7fe02ceff7a2b Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Wed, 23 Apr 2025 11:36:23 -0700 Subject: [PATCH 07/12] Update docs/questions/burning-NFTs-vs-fungibles.md Co-authored-by: Jakub Dzikowski --- docs/questions/burning-NFTs-vs-fungibles.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/questions/burning-NFTs-vs-fungibles.md b/docs/questions/burning-NFTs-vs-fungibles.md index 51c4b3e327..55dd58e692 100644 --- a/docs/questions/burning-NFTs-vs-fungibles.md +++ b/docs/questions/burning-NFTs-vs-fungibles.md @@ -59,12 +59,11 @@ const nftBurn: BurnTokenQuantity = { // For fungible tokens - quantity can be any positive number const fungibleBurn: BurnTokenQuantity = { - tokenInstanceKey: new TokenInstanceKey({ + tokenInstanceKey: TokenInstanceKey.fungibleKey({ collection: 'game-currency', category: 'coins', type: 'gold', - additionalKey: '', - instance: '0' // Usually '0' for fungibles + additionalKey: '' }), quantity: new BigNumber(1000) }; From 14b4781f256661c97c16f6f193dd0474d7d0af32 Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Wed, 23 Apr 2025 11:57:21 -0700 Subject: [PATCH 08/12] Update docs/questions/composite-keys-for-complex-queries.md Co-authored-by: Jakub Dzikowski --- docs/questions/composite-keys-for-complex-queries.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/questions/composite-keys-for-complex-queries.md b/docs/questions/composite-keys-for-complex-queries.md index 22e7a6a273..2c3264eeac 100644 --- a/docs/questions/composite-keys-for-complex-queries.md +++ b/docs/questions/composite-keys-for-complex-queries.md @@ -13,7 +13,7 @@ Composite keys in GalaChain enable efficient querying by combining multiple fiel ```typescript export class TokenBalance extends ChainObject { @Exclude() - public static readonly INDEX_KEY = 'TOKEN_BALANCE'; + public static readonly INDEX_KEY = 'GCTB'; @ChainKey({ position: 0 }) @IsUserAlias() From a4f56b1e62c1713e59926c990ed78a65b58a0bdf Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Wed, 23 Apr 2025 11:57:39 -0700 Subject: [PATCH 09/12] Update docs/questions/burn-errors-handling.md Co-authored-by: Jakub Dzikowski --- docs/questions/burn-errors-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/questions/burn-errors-handling.md b/docs/questions/burn-errors-handling.md index 058585ee33..3cfaeced91 100644 --- a/docs/questions/burn-errors-handling.md +++ b/docs/questions/burn-errors-handling.md @@ -34,7 +34,7 @@ async function handleBurnOperation(ctx: GalaChainContext, params: { } catch (error) { // Handle specific burn errors - if (error instanceof InsufficientBalanceError) { + if (ChainError.matches(InsufficientBalanceError)) { // User doesn't have enough tokens to burn const { owner, spendableQuantity, quantity, tokenInstanceKey } = error.payload; // Handle insufficient balance... From 42e17563df4e0fa80c1f283c4f71c4ac8884ac28 Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Wed, 23 Apr 2025 11:59:40 -0700 Subject: [PATCH 10/12] fix: documentation Q&A feedback, edits --- .../asynchronous-operations-in-chaincode.md | 4 ++-- docs/questions/batching-token-burns.md | 4 ++-- docs/questions/burn-errors-handling.md | 13 +++++++------ docs/questions/burn-tokens-usage.md | 3 +++ docs/questions/caching-in-chaincode.md | 4 +++- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/questions/asynchronous-operations-in-chaincode.md b/docs/questions/asynchronous-operations-in-chaincode.md index 8f6e091e8b..c192290a19 100644 --- a/docs/questions/asynchronous-operations-in-chaincode.md +++ b/docs/questions/asynchronous-operations-in-chaincode.md @@ -13,7 +13,7 @@ In GalaChain, chaincode operations are inherently asynchronous. Here's how to wo ```typescript class GameContract extends Contract { @Submit() - async processGameAction( + async ProcessGameAction( ctx: GalaChainContext, params: { gameId: string } ): Promise { @@ -69,4 +69,4 @@ async function processWithRetry( - State changes are only committed at transaction end - Use proper error handling for rollbacks -Note: While chaincode operations are async, they must be deterministic and complete within the transaction boundary. Long-running operations should be broken into multiple transactions. \ No newline at end of file +Note: While chaincode operations are async, they must be deterministic and complete within the transaction boundary. Long-running operations should be broken into multiple transactions. diff --git a/docs/questions/batching-token-burns.md b/docs/questions/batching-token-burns.md index b9cd808f4c..e3623150f2 100644 --- a/docs/questions/batching-token-burns.md +++ b/docs/questions/batching-token-burns.md @@ -28,7 +28,7 @@ async function batchBurnTokens(ctx: GalaChainContext, params: { }) { // Create array of burn quantities const toBurn: BurnTokenQuantity[] = params.burns.map(burn => ({ - tokenInstanceKey: new TokenInstanceKey({ + tokenInstanceKey: plainToInstance(TokenInstanceKey, { collection: burn.collection, category: burn.category, type: burn.type, @@ -73,4 +73,4 @@ Key Points: - The SDK handles all validation and state updates - Burns are processed sequentially for deterministic results -Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. \ No newline at end of file +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. diff --git a/docs/questions/burn-errors-handling.md b/docs/questions/burn-errors-handling.md index 058585ee33..fc8d910e8f 100644 --- a/docs/questions/burn-errors-handling.md +++ b/docs/questions/burn-errors-handling.md @@ -34,27 +34,27 @@ async function handleBurnOperation(ctx: GalaChainContext, params: { } catch (error) { // Handle specific burn errors - if (error instanceof InsufficientBalanceError) { + if (ChainError.matches(InsufficientBalanceError)) { // User doesn't have enough tokens to burn const { owner, spendableQuantity, quantity, tokenInstanceKey } = error.payload; // Handle insufficient balance... - } else if (error instanceof NftMultipleBurnNotAllowedError) { + } else if (ChainError.matches(NftMultipleBurnNotAllowedError)) { // Attempted to burn multiple instances of an NFT const { tokenInstanceKey } = error.payload; // Handle NFT burn error... - } else if (error instanceof InsufficientBurnAllowanceError) { + } else if (ChainError.matches(InsufficientBurnAllowanceError)) { // User doesn't have sufficient burn allowance const { user, allowedQuantity, quantity, tokenInstanceKey } = error.payload; // Handle insufficient allowance... - } else if (error instanceof UseAllowancesFailedError) { + } else if (ChainError.matches(UseAllowancesFailedError)) { // Failed to use burn allowances const { quantity, tokenInstanceKey, owner } = error.payload; // Handle allowance usage error... - } else if (error instanceof BurnTokensFailedError) { + } else if (ChainError.matches(BurnTokensFailedError)) { // General burn operation failure // Handle general burn error... @@ -101,5 +101,6 @@ Key Points: - Errors are strongly typed for better handling - Error messages are standardized - Error handling preserves transaction atomicity +- Errors are mapped to proper HTTP status code responses in REST API -Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. \ No newline at end of file +Note: All operations in GalaChain must be executed sequentially to ensure deterministic behavior. Never use Promise.all or other parallel execution methods, as they can lead to non-deterministic results across multiple chaincode executions. diff --git a/docs/questions/burn-tokens-usage.md b/docs/questions/burn-tokens-usage.md index d120b77daf..ec51ef9d22 100644 --- a/docs/questions/burn-tokens-usage.md +++ b/docs/questions/burn-tokens-usage.md @@ -17,6 +17,9 @@ import { TokenBurn } from '@gala-chain/api'; // TokenBurn class structure export class TokenBurn extends ChainObject { + @Exclude() + public static INDEX_KEY = "GCTBR"; + @ChainKey({ position: 0 }) public burnedBy: UserAlias; // Who performed the burn diff --git a/docs/questions/caching-in-chaincode.md b/docs/questions/caching-in-chaincode.md index e8e6a892c6..9c23e8f513 100644 --- a/docs/questions/caching-in-chaincode.md +++ b/docs/questions/caching-in-chaincode.md @@ -51,4 +51,6 @@ class GameContract extends Contract { - Reduced world state database load - Automatic cache management -Note: The stub cache is specifically designed for transaction-level caching. For cross-transaction data access patterns, consider implementing appropriate data structures and query patterns in your chaincode. \ No newline at end of file +Note: The stub cache is specifically designed for transaction-level caching. This is different from Fabric's default behavior and is not supported by default in the Hyperledger Fabric framework. + +For cross-transaction data access patterns, consider implementing appropriate data structures and query patterns in your chaincode. From 41d2ff27f3d7601714cc5ac7e61bc8b32a3e41da Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Wed, 23 Apr 2025 14:17:54 -0700 Subject: [PATCH 11/12] fix: correct links, revise Q&A --- docs/concepts/facts-not-objects.md | 2 +- .../composite-keys-for-complex-queries.md | 3 +- .../concurrent-operations-in-chaincode.md | 224 +----------------- 3 files changed, 14 insertions(+), 215 deletions(-) diff --git a/docs/concepts/facts-not-objects.md b/docs/concepts/facts-not-objects.md index 5821850a51..784208870e 100644 --- a/docs/concepts/facts-not-objects.md +++ b/docs/concepts/facts-not-objects.md @@ -157,7 +157,7 @@ In the above example, we see that moves are stored separately as individual fact 3. **Improved Observability**: By breaking down complex state changes into smaller, more manageable facts, it can be easier for downstream systems to watch for changes in specific events or conditions. -4. **Scalability**: Separating objects into multiple facts can help avoid [MVCC_READ_CONFLICTS](./mvcc_read_conflicts) and improve performance under high load. +4. **Scalability**: Separating objects into multiple facts can help avoid [MVCC_READ_CONFLICTS](./mvcc-read-conflicts.md) and improve performance under high load. ## Best Practices diff --git a/docs/questions/composite-keys-for-complex-queries.md b/docs/questions/composite-keys-for-complex-queries.md index 2c3264eeac..09b0901164 100644 --- a/docs/questions/composite-keys-for-complex-queries.md +++ b/docs/questions/composite-keys-for-complex-queries.md @@ -97,4 +97,5 @@ Best practices: - Use meaningful key parts that support your query patterns - Keep key parts short to minimize storage overhead - Consider index size impact when designing composite keys -- Document the composite key structure in class comments \ No newline at end of file +- Document the composite key structure in class comments +- Only use primitive types (e.g. string) as parts of the key. The only advanced types we currently support as part of a composite key is `BigNumber` from the `bignumber.js` library. diff --git a/docs/questions/concurrent-operations-in-chaincode.md b/docs/questions/concurrent-operations-in-chaincode.md index 9c37426c60..a028cdd5c9 100644 --- a/docs/questions/concurrent-operations-in-chaincode.md +++ b/docs/questions/concurrent-operations-in-chaincode.md @@ -6,225 +6,23 @@ How do I implement concurrent operations in chaincode? ### Answer +Generally, you won't need to think about how to implement concurrent operations in chaincode. GalaChain provides built-in mechanisms for handling concurrent operations safely. -GalaChain provides built-in mechanisms for handling concurrent operations safely. Here's how to implement and manage concurrency: +This is because Hyperledger Fabric provides built in MVCC (Multi-version concurrency control) and GalaChain builds on top of this framework to provide within-transaction atomicity. -1. Version-Based Concurrency: -```typescript -export class VersionedAsset extends ChainObject { - @ChainKey({ position: 0 }) - public readonly assetId: string; +There are other documents covering these topics; be sure to review our [high throughput minting](../high-throughput-minting.md) documentation for a specific example, and [our core concepts](../concepts/mvcc-read-conflicts.md) pages for more information. - public readonly version: number; - public readonly data: any; +The primary concern for implementing developers is to avoid triggering these built in errors. - public nextVersion(newData: any): VersionedAsset { - return new VersionedAsset({ - ...this, - version: this.version + 1, - data: newData - }); - } -} +For example, if two separate transactions occuring within the same block tried to write the same user's token balance, one of the transactions would fail with an `MVCC_READ_CONFLICT`. However, retry logic built into the Operations API will retry the failed transaction to a point and this failure may not be apparent to the end user or the implementing developer during times of normal load. -@Submit() -async function updateAsset( - ctx: GalaChainContext, - params: { - assetId: string; - expectedVersion: number; - newData: any; - } -): Promise { - const key = ChainObject.getCompositeKeyFromParts( - VersionedAsset.INDEX_KEY, - [params.assetId] - ); - - const asset = await getObjectByKey(ctx, VersionedAsset, key); - if (!asset) { - throw new Error('Asset not found'); - } +Key Concepts: - // Version check for optimistic locking - if (asset.version !== params.expectedVersion) { - throw new Error('Version mismatch - asset was modified'); - } - - // Update with new version - const updated = asset.nextVersion(params.newData); - await putChainObject(ctx, updated); -} -``` - -2. Atomic Operations: -```typescript -@Submit() -async function transferBetweenAccounts( - ctx: GalaChainContext, - params: { - fromId: string; - toId: string; - amount: number; - } -): Promise { - // All operations in this transaction are atomic - const fromBalance = await getBalance(ctx, params.fromId); - if (!fromBalance || fromBalance.amount < params.amount) { - throw new Error('Insufficient balance'); - } - - const toBalance = await getBalance(ctx, params.toId); - if (!toBalance) { - throw new Error('Recipient account not found'); - } - - fromBalance.amount -= params.amount; - toBalance.amount += params.amount; - - // Both operations will succeed or fail together, because of GalaChainStubCache handling - await putChainObject(ctx, fromBalance); - await putChainObject(ctx, toBalance); -} -``` - -3. Handling Race Conditions: -```typescript -export class LockableAsset extends ChainObject { - @ChainKey({ position: 0 }) - public readonly assetId: string; - - public readonly lockedBy?: string; - public readonly lockExpiry?: number; - public readonly data: any; - - public isLocked(): boolean { - return this.lockExpiry != null && - this.lockExpiry > Date.now(); - } - - public lock(userId: string): LockableAsset { - return new LockableAsset({ - ...this, - lockedBy: userId, - lockExpiry: Date.now() + 60000 // 1 minute lock - }); - } - - public unlock(): LockableAsset { - return new LockableAsset({ - ...this, - lockedBy: undefined, - lockExpiry: undefined - }); - } -} - -@Submit() -async function modifyWithLock( - ctx: GalaChainContext, - params: { - assetId: string; - userId: string; - modifications: any; - } -): Promise { - const key = ChainObject.getCompositeKeyFromParts( - LockableAsset.INDEX_KEY, - [params.assetId] - ); - - const asset = await getObjectByKey(ctx, LockableAsset, key); - if (!asset) { - throw new Error('Asset not found'); - } - - if (asset.isLocked() && asset.lockedBy !== params.userId) { - throw new Error('Asset is locked by another user'); - } - - // Acquire lock - const locked = asset.lock(params.userId); - await putChainObject(ctx, locked); - - try { - // Perform modifications - const modified = new LockableAsset({ - ...locked, - data: { - ...locked.data, - ...params.modifications - } - }); - - // Release lock and save changes - const unlocked = modified.unlock(); - await putChainObject(ctx, unlocked); - } catch (error) { - // Ensure lock is released on error - const unlocked = asset.unlock(); - await putChainObject(ctx, unlocked); - throw error; - } -} -``` - -4. Batch Processing with Concurrency: -```typescript -@Submit() -async function batchProcess( - ctx: GalaChainContext, - params: { - items: Array<{ - id: string; - operation: string; - data: any; - }>; - } -): Promise<{ - succeeded: string[]; - failed: Array<{ id: string; error: string }>; -}> { - const results = { - succeeded: [] as string[], - failed: [] as Array<{ id: string; error: string }> - }; - - // Process items sequentially to maintain consistency - for (const item of params.items) { - try { - await processItem(ctx, item); - results.succeeded.push(item.id); - } catch (error) { - results.failed.push({ - id: item.id, - error: error.message - }); - // Continue processing other items - } - } - - return results; -} -``` +- Generally the SDK is designed to handle concurrency. +- To succeed, transactions must execute determininsticly across peers. +- Write conflicts and race conditions are handled by the underlying blockchain technology. Best Practices: -- Use version-based concurrency -- Leverage atomic transactions -- Implement proper locking -- Handle race conditions -- Process batches carefully - -Key Points: -- All chaincode operations are atomic -- Use optimistic locking -- Implement proper error handling -- Consider transaction boundaries -- Monitor concurrent access -Concurrency Tips: -- Keep transactions short -- Handle timeouts appropriately -- Implement retry mechanisms -- Log concurrent operations -- Test concurrent scenarios \ No newline at end of file +- Follow guidlines for [chain key design](../concepts/chain-key-design.md), [data modeling](../concepts/facts-not-objects.md), and [create/read/update/delete handling](../concepts/create-read-update-delete.md). +- Avoid data structures, api services, or client applications that could exacerbate the possibility of `MVCC_READ_CONFLICT` errors. From f5edbccb9a6898eebffedacced84331459145b3b Mon Sep 17 00:00:00 2001 From: Mike Graham Date: Wed, 23 Apr 2025 14:23:42 -0700 Subject: [PATCH 12/12] fix: add caveat about range reads to Q&A --- docs/questions/caching-in-chaincode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/questions/caching-in-chaincode.md b/docs/questions/caching-in-chaincode.md index 9c23e8f513..3de7c9417b 100644 --- a/docs/questions/caching-in-chaincode.md +++ b/docs/questions/caching-in-chaincode.md @@ -51,6 +51,6 @@ class GameContract extends Contract { - Reduced world state database load - Automatic cache management -Note: The stub cache is specifically designed for transaction-level caching. This is different from Fabric's default behavior and is not supported by default in the Hyperledger Fabric framework. +Note: The stub cache is specifically designed for transaction-level caching. This is different from Fabric's default behavior and is not supported by default in the Hyperledger Fabric framework. This support covers the most common use case of partial composite keys and queries, but does not yet support range reads (`RangedChainObject` / `getStateByRange`). For cross-transaction data access patterns, consider implementing appropriate data structures and query patterns in your chaincode.