diff --git a/dcl-workspace.json b/dcl-workspace.json index 41b136b..2a33dd8 100644 --- a/dcl-workspace.json +++ b/dcl-workspace.json @@ -120,6 +120,9 @@ { "path": "scenes/10,0-move-to-test" }, + { + "path": "scenes/1,1-web3-operations" + }, { "path": "scenes/1,0-visibility-comp-propagation" }, diff --git a/scenes/1,1-web3-operations/dclcontext/crypto.mdc b/scenes/1,1-web3-operations/dclcontext/crypto.mdc new file mode 100644 index 0000000..42f3b1f --- /dev/null +++ b/scenes/1,1-web3-operations/dclcontext/crypto.mdc @@ -0,0 +1,880 @@ +# @dcl-crypto-toolkit Context7 Reference + +## Installation & Import +```typescript +// Install via npm +npm install dcl-crypto-toolkit + +// Import in your code +import * as crypto from 'dcl-crypto-toolkit' +import { executeTask } from '@dcl/sdk/ecs' +``` + +## Basic Setup + +### Initialize Crypto Toolkit +```typescript +// The crypto toolkit is ready to use after import +// All functions are async and should be wrapped in executeTask +executeTask(async () => { + // Crypto operations here +}) +``` + +## MANA Operations + +### Send MANA to Address +```typescript +// Send MANA to a specific address +executeTask(async () => { + await crypto.mana.send( + '0x1234567890123456789012345678901234567890', // toAddress + 10, // amount in MANA + true // waitConfirm (optional, default: false) + ) +}) + +// Send without waiting for confirmation +executeTask(async () => { + await crypto.mana.send( + '0x1234567890123456789012345678901234567890', + 5, + false + ) +}) +``` + +### Get Player's MANA Balance +```typescript +// Get current player's MANA balance +executeTask(async () => { + const balance = await crypto.mana.getBalance() + console.log('MANA Balance:', balance) +}) + +// Get balance for specific address +executeTask(async () => { + const balance = await crypto.mana.getBalance( + '0x1234567890123456789012345678901234567890' + ) + console.log('Address MANA Balance:', balance) +}) +``` + +## Currency Operations + +### Send Currency +```typescript +// Send any ERC20 token +executeTask(async () => { + await crypto.currency.send( + '0x1234567890123456789012345678901234567890', // contractAddress + '0x0987654321098765432109876543210987654321', // toAddress + 1000000000000000000, // amount in wei + true // waitConfirm (optional, default: false) + ) +}) +``` + +### Check Currency Balance +```typescript +// Check balance of any ERC20 token +executeTask(async () => { + const balance = await crypto.currency.getBalance( + '0x1234567890123456789012345678901234567890', // contractAddress + '0x0987654321098765432109876543210987654321' // address (optional, defaults to current player) + ) + console.log('Token Balance:', balance) +}) +``` + +### Check Currency Allowance +```typescript +// Check how much a contract can spend on behalf of a player +executeTask(async () => { + const allowance = await crypto.currency.allowance( + '0x1234567890123456789012345678901234567890', // contractAddress + '0x0987654321098765432109876543210987654321', // owner + '0x1111111111111111111111111111111111111111' // spender + ) + console.log('Allowance:', allowance) +}) +``` + +### Set Currency Approval +```typescript +// Grant permission for a contract to spend tokens +executeTask(async () => { + await crypto.currency.setApproval( + '0x1234567890123456789012345678901234567890', // contractAddress + '0x0987654321098765432109876543210987654321', // spender + true, // waitConfirm (optional, default: false) + '1000000000000000000000' // amount in wei (optional, defaults to max) + ) +}) +``` + +### Check Currency Approval Status +```typescript +// Check if a contract is approved to spend tokens +executeTask(async () => { + const isApproved = await crypto.currency.isApproved( + '0x1234567890123456789012345678901234567890', // contractAddress + '0x0987654321098765432109876543210987654321', // owner + '0x1111111111111111111111111111111111111111' // spender + ) + console.log('Is Approved:', isApproved) +}) +``` + +## NFT Operations + +### Transfer NFT +```typescript +// Transfer an NFT to another address +executeTask(async () => { + await crypto.nft.transfer( + '0x1234567890123456789012345678901234567890', // contractAddress + '0x0987654321098765432109876543210987654321', // toAddress + 123, // tokenId + true // waitConfirm (optional, default: false) + ) +}) +``` + +### Check NFT Ownership +```typescript +// Check if a player owns a specific NFT +executeTask(async () => { + const balance = await crypto.nft.getBalance( + '0x1234567890123456789012345678901234567890', // contractAddress + 123, // tokenId + '0x0987654321098765432109876543210987654321' // address (optional, defaults to current player) + ) + console.log('NFT Balance:', balance) +}) +``` + +### Check NFT Approval for All +```typescript +// Check if a contract is approved to handle all NFTs of a collection +executeTask(async () => { + const isApproved = await crypto.nft.isApprovedForAll( + '0x1234567890123456789012345678901234567890', // contractAddress + '0x0987654321098765432109876543210987654321', // assetHolder + '0x1111111111111111111111111111111111111111' // operator + ) + console.log('Is Approved For All:', isApproved) +}) +``` + +### Set NFT Approval for All +```typescript +// Grant permission for a contract to handle all NFTs of a collection +executeTask(async () => { + await crypto.nft.setApprovalForAll( + '0x1234567890123456789012345678901234567890', // contractAddress + '0x0987654321098765432109876543210987654321', // operator + true, // approved (optional, default: true) + true // waitConfirm (optional, default: false) + ) +}) +``` + +## Message Signing + +### Sign Message +```typescript +// Sign a message with the player's wallet +executeTask(async () => { + const signature = await crypto.signMessage('Hello Decentraland!') + console.log('Signature:', signature) +}) + +// Sign with custom message +executeTask(async () => { + const message = 'I agree to the terms and conditions' + const signature = await crypto.signMessage(message) + console.log('Signed message:', signature) +}) +``` + +## Decentraland Contracts + +### Access Predefined Contracts +```typescript +// Use predefined Decentraland contract addresses +console.log('MANA Token:', crypto.contract.mainnet.MANAToken) +console.log('Marketplace:', crypto.contract.mainnet.Marketplace) +console.log('LAND Registry:', crypto.contract.mainnet.LANDRegistry) + +// Halloween 2019 Collection example +console.log('Halloween Collection:', crypto.contract.mainnet.Halloween2019Collection) +``` + +### Available Contract Addresses +```typescript +// Mainnet contracts +crypto.contract.mainnet.MANAToken +crypto.contract.mainnet.Marketplace +crypto.contract.mainnet.LANDRegistry +crypto.contract.mainnet.EstateRegistry +crypto.contract.mainnet.Halloween2019Collection +crypto.contract.mainnet.Xmas2019Collection +crypto.contract.mainnet.MCHCollection +crypto.contract.mainnet.CommunityContestCollection +crypto.contract.mainnet.DappcraftMoonminerCollection +crypto.contract.mainnet.DCLLaunchCollection +crypto.contract.mainnet.DCGCollection +crypto.contract.mainnet.DCNSCollection +crypto.contract.mainnet.DGSummer2020Collection +crypto.contract.mainnet.DGFall2020Collection +crypto.contract.mainnet.DGSport2020Collection +crypto.contract.mainnet.DGAtariCollection +crypto.contract.mainnet.DGMFCollection +crypto.contract.mainnet.DGBQCollection +crypto.contract.mainnet.DGSummer2021Collection +crypto.contract.mainnet.DGWinter2021Collection +crypto.contract.mainnet.DGSpring2021Collection +crypto.contract.mainnet.DGSummer2022Collection +crypto.contract.mainnet.DGWinter2022Collection +crypto.contract.mainnet.DGSpring2022Collection +crypto.contract.mainnet.DGSummer2023Collection +crypto.contract.mainnet.DGWinter2023Collection +crypto.contract.mainnet.DGSpring2023Collection +crypto.contract.mainnet.DGSummer2024Collection +crypto.contract.mainnet.DGWinter2024Collection +crypto.contract.mainnet.DGSpring2024Collection +crypto.contract.mainnet.DGSummer2025Collection +crypto.contract.mainnet.DGWinter2025Collection +crypto.contract.mainnet.DGSpring2025Collection +crypto.contract.mainnet.DGSummer2026Collection +crypto.contract.mainnet.DGWinter2026Collection +crypto.contract.mainnet.DGSpring2026Collection +crypto.contract.mainnet.DGSummer2027Collection +crypto.contract.mainnet.DGWinter2027Collection +crypto.contract.mainnet.DGSpring2027Collection +crypto.contract.mainnet.DGSummer2028Collection +crypto.contract.mainnet.DGWinter2028Collection +crypto.contract.mainnet.DGSpring2028Collection +crypto.contract.mainnet.DGSummer2029Collection +crypto.contract.mainnet.DGWinter2029Collection +crypto.contract.mainnet.DGSpring2029Collection +crypto.contract.mainnet.DGSummer2030Collection +crypto.contract.mainnet.DGWinter2030Collection +crypto.contract.mainnet.DGSpring2030Collection +crypto.contract.mainnet.DGSummer2031Collection +crypto.contract.mainnet.DGWinter2031Collection +crypto.contract.mainnet.DGSpring2031Collection +crypto.contract.mainnet.DGSummer2032Collection +crypto.contract.mainnet.DGWinter2032Collection +crypto.contract.mainnet.DGSpring2032Collection +crypto.contract.mainnet.DGSummer2033Collection +crypto.contract.mainnet.DGWinter2033Collection +crypto.contract.mainnet.DGSpring2033Collection +crypto.contract.mainnet.DGSummer2034Collection +crypto.contract.mainnet.DGWinter2034Collection +crypto.contract.mainnet.DGSpring2034Collection +crypto.contract.mainnet.DGSummer2035Collection +crypto.contract.mainnet.DGWinter2035Collection +crypto.contract.mainnet.DGSpring2035Collection +crypto.contract.mainnet.DGSummer2036Collection +crypto.contract.mainnet.DGWinter2036Collection +crypto.contract.mainnet.DGSpring2036Collection +crypto.contract.mainnet.DGSummer2037Collection +crypto.contract.mainnet.DGWinter2037Collection +crypto.contract.mainnet.DGSpring2037Collection +crypto.contract.mainnet.DGSummer2038Collection +crypto.contract.mainnet.DGWinter2038Collection +crypto.contract.mainnet.DGSpring2038Collection +crypto.contract.mainnet.DGSummer2039Collection +crypto.contract.mainnet.DGWinter2039Collection +crypto.contract.mainnet.DGSpring2039Collection +crypto.contract.mainnet.DGSummer2040Collection +crypto.contract.mainnet.DGWinter2040Collection +crypto.contract.mainnet.DGSpring2040Collection +crypto.contract.mainnet.DGSummer2041Collection +crypto.contract.mainnet.DGWinter2041Collection +crypto.contract.mainnet.DGSpring2041Collection +crypto.contract.mainnet.DGSummer2042Collection +crypto.contract.mainnet.DGWinter2042Collection +crypto.contract.mainnet.DGSpring2042Collection +crypto.contract.mainnet.DGSummer2043Collection +crypto.contract.mainnet.DGWinter2043Collection +crypto.contract.mainnet.DGSpring2043Collection +crypto.contract.mainnet.DGSummer2044Collection +crypto.contract.mainnet.DGWinter2044Collection +crypto.contract.mainnet.DGSpring2044Collection +crypto.contract.mainnet.DGSummer2045Collection +crypto.contract.mainnet.DGWinter2045Collection +crypto.contract.mainnet.DGSpring2045Collection +crypto.contract.mainnet.DGSummer2046Collection +crypto.contract.mainnet.DGWinter2046Collection +crypto.contract.mainnet.DGSpring2046Collection +crypto.contract.mainnet.DGSummer2047Collection +crypto.contract.mainnet.DGWinter2047Collection +crypto.contract.mainnet.DGSpring2047Collection +crypto.contract.mainnet.DGSummer2048Collection +crypto.contract.mainnet.DGWinter2048Collection +crypto.contract.mainnet.DGSpring2048Collection +crypto.contract.mainnet.DGSummer2049Collection +crypto.contract.mainnet.DGWinter2049Collection +crypto.contract.mainnet.DGSpring2049Collection +crypto.contract.mainnet.DGSummer2050Collection +crypto.contract.mainnet.DGWinter2050Collection +crypto.contract.mainnet.DGSpring2050Collection +``` + +## Marketplace Operations + +### Buy Item from Marketplace +```typescript +// Buy an item from the Decentraland marketplace +executeTask(async () => { + await crypto.marketplace.buyOrder( + '0x1234567890123456789012345678901234567890', // nftAddress + 123, // assetId + '1000000000000000000' // price in wei + ) +}) +``` + +### Check Player's Marketplace Authorizations +```typescript +// Check if player has authorized marketplace to handle their tokens +executeTask(async () => { + const isAuthorized = await crypto.marketplace.isAuthorized() + console.log('Marketplace Authorized:', isAuthorized) +}) +``` + +### Sell Item from Scene +```typescript +// List an item for sale on the marketplace +executeTask(async () => { + await crypto.marketplace.sellOrder( + '0x1234567890123456789012345678901234567890', // nftAddress + 123, // assetId + '1000000000000000000', // price in wei + '1000000000000000000' // expiresAt timestamp + ) +}) +``` + +### Cancel Marketplace Order +```typescript +// Cancel a marketplace listing +executeTask(async () => { + await crypto.marketplace.cancelOrder( + '0x1234567890123456789012345678901234567890', // nftAddress + 123 // assetId + ) +}) +``` + +## Third Party Contract Operations + +### Check Currency Approval for Third Party +```typescript +// Check if a third-party contract can spend player's tokens +executeTask(async () => { + const isApproved = await crypto.currency.isApproved( + crypto.contract.mainnet.MANAToken, + '0x1234567890123456789012345678901234567890' // thirdPartyContract + ) + console.log('Third Party Approved:', isApproved) +}) +``` + +### Grant Currency Approval to Third Party +```typescript +// Allow a third-party contract to spend player's tokens +executeTask(async () => { + await crypto.currency.setApproval( + crypto.contract.mainnet.MANAToken, + '0x1234567890123456789012345678901234567890', // thirdPartyContract + true, // waitConfirm + '1000000000000000000000' // amount in wei + ) +}) +``` + +### Check NFT Approval for Third Party +```typescript +// Check if a third-party contract can handle player's NFTs +executeTask(async () => { + const isApproved = await crypto.nft.isApprovedForAll( + crypto.contract.mainnet.Halloween2019Collection, + '0x1234567890123456789012345678901234567890' // thirdPartyContract + ) + console.log('Third Party NFT Approved:', isApproved) +}) +``` + +### Grant NFT Approval to Third Party +```typescript +// Allow a third-party contract to handle player's NFTs +executeTask(async () => { + await crypto.nft.setApprovalForAll( + crypto.contract.mainnet.Halloween2019Collection, + '0x1234567890123456789012345678901234567890', // thirdPartyContract + true, // approved + true // waitConfirm + ) +}) +``` + +## Custom Contract Interactions + +### Call Any Contract Function +```typescript +// Define contract ABI (simplified example) +const LANDAbi = [ + { + "constant": true, + "inputs": [{"name": "_owner", "type": "address"}], + "name": "balanceOf", + "outputs": [{"name": "", "type": "uint256"}], + "type": "function" + } +] + +// Get contract instance +executeTask(async () => { + const contract = await crypto.contract.getContract( + '0xF87E31492Faf9A91B02Ee0dEAAd50d51d56D5d4d', // contractAddress + LANDAbi + ) + + // Call contract function + const balance = await contract.balanceOf('0x1234567890123456789012345678901234567890') + console.log('LAND Balance:', balance) +}) +``` + +### Get Contract with Request Manager +```typescript +// Get contract with additional request manager for more control +executeTask(async () => { + const { contract, requestManager } = await crypto.contract.getContract( + crypto.contract.mainnet.MANAToken + ) + + // Use request manager for custom transaction handling + const balance = await contract.balanceOf('0x1234567890123456789012345678901234567890') + console.log('MANA Balance:', balance) +}) +``` + +## Wearable Data + +### Get List of Wearables +```typescript +// Get all wearables +executeTask(async () => { + const wearables = await crypto.wearable.getListOfWearables() + console.log('All Wearables:', wearables) +}) + +// Get wearables with filters +executeTask(async () => { + const filteredWearables = await crypto.wearable.getListOfWearables({ + collectionIds: ['urn:decentraland:ethereum:collections-v1:mf_sammichgamer'], + wearableIds: ['urn:decentraland:ethereum:collections-v1:mf_sammichgamer:mf_sammichgamer'], + textSearch: 'sammich' + }) + console.log('Filtered Wearables:', filteredWearables) +}) +``` + +### Wearable Filter Options +```typescript +interface WearableFilters { + collectionIds?: string[] // Filter by collection URNs + wearableIds?: string[] // Filter by specific wearable URNs + textSearch?: string // Search by text in wearable name/description +} +``` + +## Common Crypto Patterns + +### Complete Payment Flow +```typescript +// Complete payment flow with approval checks +async function processPayment(amount: number, recipient: string) { + try { + // Check MANA balance + const balance = await crypto.mana.getBalance() + if (balance < amount) { + throw new Error('Insufficient MANA balance') + } + + // Send MANA + await crypto.mana.send(recipient, amount, true) + + console.log(`Payment of ${amount} MANA sent to ${recipient}`) + return true + } catch (error) { + console.error('Payment failed:', error) + return false + } +} + +// Usage +executeTask(async () => { + const success = await processPayment(10, '0x1234567890123456789012345678901234567890') + if (success) { + console.log('Payment successful!') + } +}) +``` + +### NFT Trading System +```typescript +// Complete NFT trading system +class NFTTradingSystem { + async checkOwnership(contractAddress: string, tokenId: number): Promise { + const balance = await crypto.nft.getBalance(contractAddress, tokenId) + return balance > 0 + } + + async transferNFT(contractAddress: string, toAddress: string, tokenId: number): Promise { + try { + const ownsNFT = await this.checkOwnership(contractAddress, tokenId) + if (!ownsNFT) { + throw new Error('Player does not own this NFT') + } + + await crypto.nft.transfer(contractAddress, toAddress, tokenId, true) + console.log(`NFT ${tokenId} transferred to ${toAddress}`) + return true + } catch (error) { + console.error('NFT transfer failed:', error) + return false + } + } + + async listForSale(contractAddress: string, tokenId: number, price: string): Promise { + try { + // Check marketplace authorization + const isAuthorized = await crypto.marketplace.isAuthorized() + if (!isAuthorized) { + // Grant authorization + await crypto.nft.setApprovalForAll(contractAddress, crypto.contract.mainnet.Marketplace, true, true) + } + + // List for sale + const expiresAt = Date.now() + (7 * 24 * 60 * 60 * 1000) // 7 days from now + await crypto.marketplace.sellOrder(contractAddress, tokenId, price, expiresAt.toString()) + + console.log(`NFT ${tokenId} listed for sale at ${price} wei`) + return true + } catch (error) { + console.error('Listing failed:', error) + return false + } + } +} + +// Usage +const tradingSystem = new NFTTradingSystem() + +executeTask(async () => { + // Transfer NFT + await tradingSystem.transferNFT( + '0x1234567890123456789012345678901234567890', + '0x0987654321098765432109876543210987654321', + 123 + ) + + // List for sale + await tradingSystem.listForSale( + '0x1234567890123456789012345678901234567890', + 123, + '1000000000000000000' // 1 MANA in wei + ) +}) +``` + +### Marketplace Integration +```typescript +// Complete marketplace integration +class MarketplaceIntegration { + async buyItem(nftAddress: string, assetId: number, price: string): Promise { + try { + // Check if player has enough MANA + const balance = await crypto.mana.getBalance() + const priceInMana = parseFloat(price) / 1e18 // Convert from wei to MANA + + if (balance < priceInMana) { + throw new Error('Insufficient MANA balance') + } + + // Check marketplace authorization + const isAuthorized = await crypto.marketplace.isAuthorized() + if (!isAuthorized) { + // Grant authorization + await crypto.mana.setApproval(crypto.contract.mainnet.Marketplace, true, price) + } + + // Buy the item + await crypto.marketplace.buyOrder(nftAddress, assetId, price) + + console.log(`Successfully purchased NFT ${assetId} for ${priceInMana} MANA`) + return true + } catch (error) { + console.error('Purchase failed:', error) + return false + } + } + + async cancelListing(nftAddress: string, assetId: number): Promise { + try { + await crypto.marketplace.cancelOrder(nftAddress, assetId) + console.log(`Cancelled listing for NFT ${assetId}`) + return true + } catch (error) { + console.error('Cancellation failed:', error) + return false + } + } +} + +// Usage +const marketplace = new MarketplaceIntegration() + +executeTask(async () => { + // Buy an item + await marketplace.buyItem( + '0x1234567890123456789012345678901234567890', + 123, + '1000000000000000000' // 1 MANA in wei + ) + + // Cancel a listing + await marketplace.cancelListing( + '0x1234567890123456789012345678901234567890', + 123 + ) +}) +``` + +## Error Handling + +### Common Error Patterns +```typescript +// Handle common crypto operation errors +async function safeCryptoOperation(operation: () => Promise) { + try { + const result = await operation() + return { success: true, result } + } catch (error: any) { + // Handle specific error types + if (error.message.includes('insufficient funds')) { + console.error('Insufficient balance for transaction') + return { success: false, error: 'INSUFFICIENT_BALANCE' } + } else if (error.message.includes('user rejected')) { + console.error('User rejected the transaction') + return { success: false, error: 'USER_REJECTED' } + } else if (error.message.includes('network error')) { + console.error('Network error occurred') + return { success: false, error: 'NETWORK_ERROR' } + } else { + console.error('Unknown error:', error) + return { success: false, error: 'UNKNOWN_ERROR' } + } + } +} + +// Usage +executeTask(async () => { + const result = await safeCryptoOperation(async () => { + return await crypto.mana.send('0x1234567890123456789012345678901234567890', 1, true) + }) + + if (result.success) { + console.log('Operation successful:', result.result) + } else { + console.log('Operation failed:', result.error) + } +}) +``` + +## Performance Optimization + +### Batch Operations +```typescript +// Batch multiple crypto operations +class CryptoBatchProcessor { + private operations: Array<() => Promise> = [] + + addOperation(operation: () => Promise) { + this.operations.push(operation) + } + + async executeAll(): Promise> { + const results = [] + + for (const operation of this.operations) { + try { + const result = await operation() + results.push({ success: true, result }) + } catch (error: any) { + results.push({ success: false, error: error.message }) + } + } + + this.operations = [] // Clear operations after execution + return results + } +} + +// Usage +const batchProcessor = new CryptoBatchProcessor() + +executeTask(async () => { + // Add multiple operations + batchProcessor.addOperation(async () => { + return await crypto.mana.getBalance() + }) + + batchProcessor.addOperation(async () => { + return await crypto.nft.getBalance('0x1234567890123456789012345678901234567890', 123) + }) + + batchProcessor.addOperation(async () => { + return await crypto.currency.getBalance('0x1234567890123456789012345678901234567890') + }) + + // Execute all operations + const results = await batchProcessor.executeAll() + console.log('Batch results:', results) +}) +``` + +## Best Practices + +### Security Guidelines +```typescript +// 1. Always validate addresses +function isValidAddress(address: string): boolean { + return /^0x[a-fA-F0-9]{40}$/.test(address) +} + +// 2. Always validate amounts +function isValidAmount(amount: number): boolean { + return amount > 0 && Number.isFinite(amount) +} + +// 3. Use proper error handling +async function secureCryptoOperation(operation: () => Promise) { + try { + return await operation() + } catch (error: any) { + console.error('Crypto operation failed:', error) + throw new Error(`Operation failed: ${error.message}`) + } +} + +// 4. Validate user input +function validateTransactionInput(toAddress: string, amount: number): boolean { + if (!isValidAddress(toAddress)) { + throw new Error('Invalid recipient address') + } + + if (!isValidAmount(amount)) { + throw new Error('Invalid amount') + } + + return true +} +``` + +### Memory Management +```typescript +// Clean up crypto operations when scene changes +class CryptoManager { + private activeOperations: Array> = [] + + addOperation(operation: Promise) { + this.activeOperations.push(operation) + } + + async waitForAll(): Promise { + await Promise.all(this.activeOperations) + this.activeOperations = [] + } + + cancelAll(): void { + this.activeOperations = [] + } +} + +// Usage +const cryptoManager = new CryptoManager() + +// Add operations +cryptoManager.addOperation(crypto.mana.send('0x1234567890123456789012345678901234567890', 1, false)) +cryptoManager.addOperation(crypto.nft.getBalance('0x1234567890123456789012345678901234567890', 123)) + +// Wait for all operations to complete +await cryptoManager.waitForAll() +``` + +### Transaction Monitoring +```typescript +// Monitor transaction status +class TransactionMonitor { + async monitorTransaction(txHash: string, maxAttempts: number = 10): Promise { + let attempts = 0 + + while (attempts < maxAttempts) { + try { + // Check transaction status (implementation depends on your setup) + const status = await this.checkTransactionStatus(txHash) + + if (status === 'confirmed') { + console.log(`Transaction ${txHash} confirmed`) + return true + } else if (status === 'failed') { + console.log(`Transaction ${txHash} failed`) + return false + } + + // Wait before next check + await new Promise(resolve => setTimeout(resolve, 5000)) // 5 seconds + attempts++ + } catch (error) { + console.error('Error checking transaction status:', error) + attempts++ + } + } + + console.log(`Transaction ${txHash} status unknown after ${maxAttempts} attempts`) + return false + } + + private async checkTransactionStatus(txHash: string): Promise { + // Implement transaction status checking logic + // This would typically involve calling a blockchain API + return 'pending' + } +} + +// Usage +const monitor = new TransactionMonitor() + +executeTask(async () => { + const txHash = await crypto.mana.send('0x1234567890123456789012345678901234567890', 1, false) + const confirmed = await monitor.monitorTransaction(txHash) + + if (confirmed) { + console.log('Transaction confirmed successfully') + } else { + console.log('Transaction failed or timed out') + } +}) +description: +globs: +alwaysApply: false +--- diff --git a/scenes/1,1-web3-operations/dclcontext/npc.mdc b/scenes/1,1-web3-operations/dclcontext/npc.mdc new file mode 100644 index 0000000..3ea5656 --- /dev/null +++ b/scenes/1,1-web3-operations/dclcontext/npc.mdc @@ -0,0 +1,716 @@ +# dcl-npc-toolkit Context7 Reference + +## Installation & Import + +To install the NPC toolkit, run the following command: + +```bash +npm i dcl-npc-toolkit +``` + +Then import the toolkit in your code: + +```typescript +import { createNPC, Dialog } from 'dcl-npc-toolkit' +``` + +Then you can use the toolkit to create NPCs. + + +## Basic NPC Creation + +### Create Simple NPC +```typescript +const npcEntity = engine.addEntity() + +// Add avatar shape component +AvatarShape.create(npcEntity, { + id: 'npc-001', // Required: unique identifier + name: 'Guide NPC', // Display name (optional, default: "NPC") + bodyShape: 'urn:decentraland:off-chain:base-avatars:BaseMale', // Optional + wearables: [ // Optional: array of wearable URNs + 'urn:decentraland:off-chain:base-avatars:blue_tshirt', + 'urn:decentraland:off-chain:base-avatars:brown_pants' + ], + emotes: [], // Optional: array of emote URNs + eyeColor: Color3.create(0.3, 0.7, 0.9), // Optional + skinColor: Color3.create(0.8, 0.6, 0.5), // Optional + hairColor: Color3.create(0.1, 0.1, 0.1), // Optional + talking: false // Optional: shows voice chat indicator +}) + +// Position the NPC +Transform.create(npcEntity, { + position: Vector3.create(8, 0.25, 8), + rotation: { x: 0, y: 0, z: 0, w: 1 } +}) +``` + +### Create NPC with Random Appearance +```typescript +const randomNPC = engine.addEntity() +AvatarShape.create(randomNPC, { + id: 'random-npc-' + Math.random().toString(36).substr(2, 9) +}) +Transform.create(randomNPC, { + position: Vector3.create(4, 0.25, 4) +}) +``` + +### Create NPC with toolkit (GLB + dialog) +```typescript +// Use the toolkit to instantiate an NPC from a GLB with a simple dialog and activation callback +const guide = createNPC( + { + position: { x: 8, y: 0, z: 8 }, + rotation: { x: 0, y: 0, z: 0, w: 1 }, + scale: { x: 1, y: 1, z: 1 } + }, + { + type: 'custom', + model: 'models/guide.glb', + dialog: [{ text: "Welcome!", isEndOfDialog: true } as Dialog], + onActivate: () => { + // Called on click; open dialog or run logic + console.log('Guide activated') + } + } +) +``` + +## NPC Animations & Emotes + +### Play Predefined Emotes +```typescript +// Set NPC to play a predefined emote +AvatarShape.create(npc, { + id: 'animated-npc', + expressionTriggerId: 'wave' // 'clap', 'dance', 'robot', etc. +}) + +// Change emote dynamically +AvatarShape.getMutable(npc).expressionTriggerId = 'clap' +``` + +### Available Predefined Emotes +```typescript +// Social emotes +'wave', 'fistpump', 'robot', 'raiseHand', 'clap', 'money', 'kiss', 'tik', 'hammer', 'tektonik', 'dontsee', 'handsair', 'shrug', 'disco', 'dab', 'headexplode' + +// Action emotes +'buttonDown', 'buttonFront', 'getHit', 'knockOut', 'lever', 'openChest', 'openDoor', 'punch', 'push', 'swingWeaponOneHand', 'swingWeaponTwoHands', 'throw' + +// Sitting emotes +'sittingChair1', 'sittingChair2', 'sittingGround1', 'sittingGround2' +``` + +### Custom Emotes (Advanced) +```typescript +// Note: Custom emotes require .glb files ending with _emote.glb +// This is currently limited and may not work with NPCs +AvatarShape.create(npc, { + id: 'custom-npc', + emotes: [ + 'urn:decentraland:ethereum:erc721:0xcontract:tokenId' // NFT emote URN + ] +}) +``` + +## NPC Appearance Management + +### Copy Player Appearance +```typescript +function copyPlayerAppearance(npcEntity: Entity) { + const playerData = getPlayer() + + if (!playerData || !playerData.wearables) return + + const mutableNPC = AvatarShape.getMutable(npcEntity) + + // Copy wearables + mutableNPC.wearables = playerData.wearables + + // Copy avatar base properties + mutableNPC.bodyShape = playerData.avatar?.bodyShapeUrn + mutableNPC.eyeColor = playerData.avatar?.eyesColor + mutableNPC.skinColor = playerData.avatar?.skinColor + mutableNPC.hairColor = playerData.avatar?.hairColor +} +``` + +### Update NPC Properties +```typescript +// Update NPC name +AvatarShape.getMutable(npc).name = 'New Name' + +// Update talking status +AvatarShape.getMutable(npc).talking = true + +// Update wearables +AvatarShape.getMutable(npc).wearables = [ + 'urn:decentraland:off-chain:base-avatars:red_tshirt', + 'urn:decentraland:off-chain:base-avatars:black_pants' +] + +// Update colors +AvatarShape.getMutable(npc).eyeColor = Color3.create(1, 0, 0) // Red eyes +AvatarShape.getMutable(npc).skinColor = Color3.create(0.9, 0.8, 0.7) +AvatarShape.getMutable(npc).hairColor = Color3.create(0.8, 0.6, 0.4) +``` + +## NPC Interaction Systems + +### Click Interaction +```typescript +import { pointerEventsSystem, InputAction } from '@dcl/sdk/ecs' + +// Add click interaction to NPC +pointerEventsSystem.onPointerDown( + { + entity: npc, + opts: { + button: InputAction.IA_POINTER, + hoverText: 'Talk to NPC' + } + }, + () => { + console.log('NPC clicked!') + // Handle NPC interaction + handleNPCInteraction(npc) + } +) +``` + +### Proximity Interaction (TriggerArea) +```typescript +// Use SDK7 TriggerArea directly (no @dcl-sdk/utils) +import { TriggerArea, triggerAreaEventsSystem, ColliderLayer } from '@dcl/sdk/ecs' + +// Add a spherical trigger around the NPC +TriggerArea.setSphere(npcEntity, ColliderLayer.CL_PLAYER) +Transform.createOrReplace(npcEntity, { + position: Transform.get(npcEntity).position, + scale: Vector3.create(3, 3, 3) // radius ~3 +}) + +// Listen for player entering/leaving +triggerAreaEventsSystem.onTriggerEnter(npcEntity, () => { + showNPCDialogue(npcEntity) +}) +triggerAreaEventsSystem.onTriggerExit(npcEntity, () => { + hideNPCDialogue() +}) +``` + +## NPC Dialogue Systems + +### Simple Text Dialogue +```typescript +// Create a talking NPC using the toolkit's Dialog[] +const greeter = createNPC( + { position: { x: 8, y: 0, z: 8 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } }, + { + type: 'custom', + model: 'models/greeter.glb', + dialog: [ + { text: "Hello! I'm your guide. How can I help you today?", isEndOfDialog: true } as Dialog + ], + onActivate: () => { + // Optional extra behavior when clicked + console.log('Greeter activated') + } + } +) +``` + +### Multi-Choice Dialogue +```typescript +// Create an NPC with a multi-choice dialog flow via toolkit Dialog[] +const questGiver = createNPC( + { position: { x: 10, y: 0, z: 10 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } }, + { + type: 'custom', + model: 'models/quest_giver.glb', + dialog: [ + { + text: 'Welcome, traveler! What do you seek?', + isQuestion: true, + buttons: [ + { label: 'Tell me about this place', goToDialog: 1 }, + { label: 'Give me a quest', goToDialog: 2 }, + { label: 'Goodbye', goToDialog: 3 } + ] + } as unknown as Dialog, + { text: 'These lands are rich with secrets and stories.', isEndOfDialog: true } as Dialog, + { text: 'Take this task and return victorious!', isEndOfDialog: true } as Dialog, + { text: 'Safe travels!', isEndOfDialog: true } as Dialog + ] + } +) +``` + +### Toolkit Dialog UI vs Bubble UI (SDK7) + +When opening dialogs on an EXISTING entity (not created via `createNPC`), the toolkit expects internal per-NPC data to exist. Otherwise, UI helpers (e.g., `getTheme`, `displayDialog`) may crash. + +- UI Dialog (React HUD) + 1) Mount the toolkit UI once in your scene UI: + ```typescript + import { NpcUtilsUi } from 'dcl-npc-toolkit' + + ReactEcsRenderer.setUiRenderer(() => ( + + + + )) + ``` + 2) Ensure the NPC has toolkit dialog data before opening a window: + ```typescript + import { addDialog } from 'dcl-npc-toolkit/dist/dialog' + import { openDialogWindow } from 'dcl-npc-toolkit' + import { npcDataComponent } from 'dcl-npc-toolkit/dist/npc' + + function ensureNpcToolkitData(entity: Entity) { + if (npcDataComponent.has(entity)) return + npcDataComponent.set(entity as any, { + introduced: false, + inCooldown: false, + coolDownDuration: 5, + faceUser: undefined, + walkingSpeed: 2, + walkingAnim: undefined, + pathData: undefined, + currentPathData: [], + manualStop: false, + pathIndex: 0, + state: 'STANDING', + idleAnim: 'Idle', + hasBubble: false, + turnSpeed: 2, + theme: 'https://decentraland.org/images/ui/light-atlas-v3.png', + bubbleXOffset: 0, + bubbleYOffset: 0, + lastPlayedAnim: 'Idle' + }) + } + + // On setup + addDialog(npcEntity) // required for dialog state + ensureNpcToolkitData(npcEntity) // required for UI helpers + + // On click + openDialogWindow(npcEntity, dialogs, startIndex) + ``` + +- Bubble Dialog (world-space speech bubbles) + 1) Initialize a bubble instance per entity BEFORE calling `talkBubble`: + ```typescript + import { addDialog } from 'dcl-npc-toolkit/dist/dialog' + import { createDialogBubble } from 'dcl-npc-toolkit/dist/bubble' + import { talkBubble } from 'dcl-npc-toolkit' + + addDialog(npcEntity) + createDialogBubble(npcEntity) + talkBubble(npcEntity, dialogs, startIndex) + ``` + +Common symptoms and fixes: +- Error: "Cannot set properties of undefined (setting 'script')" in bubble.js → Missing `createDialogBubble(npc)` before `talkBubble`. +- Error in `` caused by `getTheme`/`displayDialog` → Missing `npcDataComponent` for that entity; call `addDialog` and ensure a minimal `npcDataComponent.set(...)` exists before `openDialogWindow`. + +## NPC Movement & Behavior + +### Simple Path Following +```typescript +function createPatrollingNPC() { + // Create NPC via toolkit (handles walk/idle animations) + const patrollingNPC = createNPC( + { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } }, + { type: 'custom', model: 'models/guard.glb' } + ) + + // Define patrol path + const patrolPath = [ + Vector3.create(0, 0.25, 0), + Vector3.create(5, 0.25, 0), + Vector3.create(5, 0.25, 5), + Vector3.create(0, 0.25, 5), + Vector3.create(0, 0.25, 0) + ] + + // Start walking animation via toolkit (handles walk/idle clips) + // import * as npc from 'dcl-npc-toolkit' + npc.playAnimation(patrollingNPC, 'Walk', true) + + // Patrol using Tween helpers; keep looping + Tween.setMove(patrollingNPC, patrolPath[0], patrolPath[1], 4000, EasingFunction.EF_LINEAR) + TweenSequence.create(patrollingNPC, { + sequence: [ + { duration: 4000, easingFunction: EasingFunction.EF_LINEAR, mode: Tween.Mode.Move({ start: patrolPath[1], end: patrolPath[2] }) }, + { duration: 4000, easingFunction: EasingFunction.EF_LINEAR, mode: Tween.Mode.Move({ start: patrolPath[2], end: patrolPath[3] }) }, + { duration: 4000, easingFunction: EasingFunction.EF_LINEAR, mode: Tween.Mode.Move({ start: patrolPath[3], end: patrolPath[4] }) } + ], + loop: TweenLoop.TL_RESTART + }) + + return patrollingNPC +} +``` + +### NPC Following Player +```typescript +function createFollowingNPC() { + // Create NPC via toolkit + const followingNPC = createNPC( + { position: { x: 2, y: 0, z: 2 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } }, + { type: 'custom', model: 'models/companion.glb' } + ) + + // System to make NPC follow player (read player via engine.PlayerEntity) + engine.addSystem(() => { + if (!Transform.has(engine.PlayerEntity)) return + const playerPos = Transform.get(engine.PlayerEntity).position + + const npcTransform = Transform.getMutable(followingNPC) + const currentPos = npcTransform.position + + // Calculate direction to player + const direction = Vector3.subtract(playerPos, currentPos) + const distance = Vector3.length(direction) + + // Only follow if player is within range + if (distance > 3 && distance < 10) { + const normalizedDir = Vector3.normalize(direction) + const newPos = Vector3.add(currentPos, Vector3.scale(normalizedDir, 0.05)) + npcTransform.position = newPos + + // Make NPC face player + const lookAtRotation = Quaternion.lookRotation(normalizedDir) + npcTransform.rotation = lookAtRotation + } + }) + + return followingNPC +} +``` + +## NPC State Management + +### Toolkit-managed NPC state +```typescript +// The toolkit tracks basic interaction state and dialog progression internally. +// Prefer defining behavior via createNPC options and Dialog[] rather than custom components. + +const merchant = createNPC( + { position: { x: 8, y: 0, z: 8 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } }, + { + type: 'custom', + model: 'models/merchant.glb', + dialog: [ + { text: 'Welcome to my shop!', isEndOfDialog: true } as Dialog + ], + onActivate: () => { + // Optional: extra side effects on interaction + console.log('Merchant activated') + } + } +) +``` + +### Interaction flow +```typescript +// Clicks are handled via the toolkit's onActivate; use TriggerArea for proximity prompts if needed. +const helper = createNPC( + { position: { x: 6, y: 0, z: 6 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } }, + { + type: 'custom', model: 'models/helper.glb', + dialog: [{ text: 'Need assistance?', isEndOfDialog: true } as Dialog], + onActivate: () => console.log('Helper clicked') + } +) +``` + +## NPC Multiplayer Considerations + +### Syncing NPC State +```typescript +import { syncEntity } from '@dcl/sdk/network' + +function createSyncedNPC() { + const syncedNPC = engine.addEntity() + + AvatarShape.create(syncedNPC, { + id: 'synced-npc', + name: 'Multiplayer NPC' + }) + + Transform.create(syncedNPC, { + position: Vector3.create(4, 0.25, 4) + }) + + // Sync NPC position and state across all players + syncEntity(syncedNPC, [ + Transform.componentId, + AvatarShape.componentId + ], 1) // Unique network ID + + return syncedNPC +} +``` + +### NPC Message Bus Communication +```typescript +import { MessageBus } from '@dcl/sdk/message-bus' + +const messageBus = new MessageBus() + +// Send NPC interaction message +function sendNPCInteraction(npcId: string, playerId: string, action: string) { + messageBus.emit('npc-interaction', { + npcId, + playerId, + action, + timestamp: Date.now() + }) +} + +// Listen for NPC interactions +messageBus.on('npc-interaction', (message) => { + console.log(`NPC ${message.npcId} interacted by ${message.playerId} with action ${message.action}`) + // Handle the interaction for all players +}) +``` + +## Common NPC Patterns + +### NPC with Multiple Dialogue States +```typescript +function createAdvancedNPC() { + // Multi-step dialog with branching via toolkit only + return createNPC( + { position: { x: 8, y: 0, z: 8 }, rotation: { x: 0, y: 0, z: 0, w: 1 }, scale: { x: 1, y: 1, z: 1 } }, + { + type: 'custom', model: 'models/quest_giver.glb', + dialog: [ + { text: 'Hello traveler! I have a quest for you.', isQuestion: true, buttons: [ + { label: 'Tell me about the quest', goToDialog: 1 }, + { label: 'I\'ll pass for now', goToDialog: 2 } + ] } as unknown as Dialog, + { text: 'Retrieve the ancient token from the ruins to the east.', isEndOfDialog: true } as Dialog, + { text: 'Come back if you change your mind!', isEndOfDialog: true } as Dialog + ] + } + ) +} +``` + +### NPC with Inventory/Shop System +```typescript +interface ShopItem { + id: string + name: string + price: number + description: string +} + +function createShopNPC() { + const shopNPC = engine.addEntity() + + AvatarShape.create(shopNPC, { + id: 'shop-npc', + name: 'Merchant' + }) + + Transform.create(shopNPC, { + position: Vector3.create(8, 0.25, 8) + }) + + // Add shop interaction + pointerEventsSystem.onPointerDown( + { + entity: shopNPC, + opts: { button: InputAction.IA_POINTER, hoverText: 'Open Shop' } + }, + () => openShop(shopNPC) + ) + + return shopNPC +} + +function openShop(npcEntity: Entity) { + const shopItems: ShopItem[] = [ + { id: 'sword', name: 'Magic Sword', price: 100, description: 'A powerful weapon' }, + { id: 'potion', name: 'Health Potion', price: 25, description: 'Restores health' }, + { id: 'shield', name: 'Iron Shield', price: 75, description: 'Provides protection' } + ] + + ReactEcsRenderer.setUiRenderer(() => ( + +