package block import ( "strconv" "chain" "chain/banker" "chain/runtime" "gno.land/p/demo/tokens/grc721" "gno.land/p/nt/avl" "gno.land/p/nt/ufmt" "gno.land/r/akkadia/admin" "gno.land/r/akkadia/chunk" personalworld "gno.land/r/akkadia/personal_world" ) const ( BlockInstalledEvent = "BlockInstalled" BlockUninstalledEvent = "BlockUninstalled" BlockUsedEvent = "BlockUsed" ) var ( nextLogID uint64 = 1 blockUseLogs = avl.NewTree() // logID -> map[string]string (position, tid, user, installer, fee, etc) userLogIndex = avl.NewTree() // user -> []uint64 (logIDs) ) // Install installs a block at position with handler integration func Install(cur realm, position string, blockID uint32, content string, option string) { caller := runtime.PreviousRealm().Address() // 1. Check world permission if !hasInstallPermission(caller, position) { panic("unauthorized: no permission to install block in this world") } // 2. Check if already installed if _, exists := getInstalled(position); exists { panic("block already installed at position") } // 3. Get block metadata blockIDStr := blockIDToString(blockID) blockIDKey := blockIDToKey(blockID) blockVal, found := blocks.Get(blockIDKey) if !found { panic("block not found: " + blockIDStr) } block := blockVal.(map[string]string) tokenID := blockIDToTokenID(blockID) // 4. Get GRC1155 balance balance := BalanceOf(caller, tokenID) if balance < 1 { panic("insufficient block balance: block " + blockIDStr + " requires at least 1") } // 5. Burn GRC1155 burn(caller, tokenID, 1) // 6. Save installed block shape := block["shape"] state := block["state"] saveInstalled(position, blockID, content, option, caller, shape, state) // 7. Emit event chain.Emit(BlockInstalledEvent, "position", position, "blockId", blockIDStr, "content", content, "option", option, "installer", caller.String(), ) } // Uninstall uninstalls a block at position func Uninstall(cur realm, position string) { caller := runtime.PreviousRealm().Address() // 1. Check world permission if !hasUninstallPermission(caller, position) { panic("unauthorized: no permission to uninstall block in this world") } // 2. Check if block exists mustGetInstalled(position) // 3. Remove from storage removeInstalled(position) // 4. Emit event chain.Emit(BlockUninstalledEvent, "position", position, ) } // Read reads block data at position (query-only) func Read(position string) string { // Get installed block info := mustGetInstalled(position) // Return content field return info["content"] } // Use uses a block at position and updates its option func Use(cur realm, position string) { caller := runtime.PreviousRealm().Address() // 1. Get installed block info := mustGetInstalled(position) // 2. Get block tid blockIDStr := info["blockId"] blockID := stringToBlockID(blockIDStr) blockKey := blockIDToKey(blockID) // 3. Get block properties blockVal, found := blocks.Get(blockKey) if !found { panic("block not found: " + blockIDStr) } block := blockVal.(map[string]string) // 4. Get usePrice and installerBps from block usePriceStr, found := block["usePrice"] if !found { panic("usePrice not found in block") } usePrice, err := strconv.ParseInt(usePriceStr, 10, 64) if err != nil { panic("invalid usePrice: " + err.Error()) } installerBpsStr, found := block["installerBps"] if !found { panic("installerBps not found in block") } installerBps, err := strconv.Atoi(installerBpsStr) if err != nil { panic("invalid installerBps: " + err.Error()) } // 5. Check payment sent := banker.OriginSend() paidAmount := sent.AmountOf("ugnot") if paidAmount < usePrice { panic("insufficient payment: required " + ufmt.Sprintf("%d", usePrice) + " ugnot") } // 6. Get installer installer := address(info["installer"]) // 7. Distribute funds installerShare, operatorShare := distributeFunds(installer, usePrice, installerBps) // 8. Refund excess if paidAmount > usePrice { bnk := banker.NewBanker(banker.BankerTypeRealmSend) realmAddr := runtime.CurrentRealm().Address() refund := paidAmount - usePrice coins := chain.Coins{chain.Coin{"ugnot", refund}} bnk.SendCoins(realmAddr, caller, coins) } // 9. Create use log logID := getNextLogID() logIDStr := logIDToString(logID) log := map[string]string{ "id": logIDStr, "position": position, "blockId": blockIDStr, "user": caller.String(), "installer": installer.String(), "fee": ufmt.Sprintf("%d", usePrice), "installerShare": ufmt.Sprintf("%d", installerShare), "operatorShare": ufmt.Sprintf("%d", operatorShare), "height": ufmt.Sprintf("%d", runtime.ChainHeight()), } blockUseLogs.Set(logIDStr, log) // 10. Update user log index addUserLogIndex(caller, logID) // 11. Emit event chain.Emit(BlockUsedEvent, "logId", logIDStr, "position", position, "blockId", blockIDStr, "user", caller.String(), "fee", ufmt.Sprintf("%d", usePrice), ) } // addUserLogIndex adds a log entry to user's log index func addUserLogIndex(user address, logID uint64) { userStr := user.String() logs, found := userLogIndex.Get(userStr) var logIDs []uint64 if found { logIDs = logs.([]uint64) } logIDs = append(logIDs, logID) userLogIndex.Set(userStr, logIDs) } func distributeFunds(installer address, amount int64, installerBPS int) (int64, int64) { // Calculate shares (BPS) installerShare := (amount * int64(installerBPS)) / 10000 operatorShare := amount - installerShare // Send funds to operator and protocol bnk := banker.NewBanker(banker.BankerTypeRealmSend) realmAddr := runtime.CurrentRealm().Address() if installerShare > 0 { coins := chain.Coins{chain.Coin{"ugnot", installerShare}} bnk.SendCoins(realmAddr, installer, coins) } if operatorShare > 0 { coins := chain.Coins{chain.Coin{"ugnot", operatorShare}} bnk.SendCoins(realmAddr, admin.GetFeeCollector(), coins) } return installerShare, operatorShare } // ListInstalled returns installed blocks for given positions // position format: "{type}|{worldId}|{owner}|{xIndex}|{zIndex}|{blockIndex}" func ListInstalled(positions ...string) []map[string]string { result := []map[string]string{} for _, position := range positions { store := getStore(position) storeKey, chunkCoord, blockIndex := parsePosition(position) world, found := store.Get(storeKey) if !found { continue } worldMap := world.(map[string]map[string]map[string]string) chunkMap, found := worldMap[chunkCoord] if !found { continue } info, found := chunkMap[blockIndex] if !found { continue } result = append(result, info) } return result } // ListUseLogs returns user's block use logs (newest first, limited) func ListUseLogs(user address, limit int) []map[string]string { result := []map[string]string{} if limit <= 0 { return result } userStr := user.String() logs, found := userLogIndex.Get(userStr) if !found { return result } logIDs := logs.([]uint64) // Reverse iterate (newest first) count := 0 for i := len(logIDs) - 1; i >= 0; i-- { if count >= limit { break } logID := logIDs[i] // Get log details from blockUseLogs logIDKey := logIDToString(logID) logData, found := blockUseLogs.Get(logIDKey) if !found { continue } log := logData.(map[string]string) result = append(result, log) count++ } return result } func getNextLogID() uint64 { id := nextLogID nextLogID++ return id } func logIDToString(logID uint64) string { return ufmt.Sprintf("%d", logID) } // HasInstalled checks if a block is installed at position func HasInstalled(position string) bool { _, exists := getInstalled(position) return exists } // hasInstallPermission checks if caller has world permission to install blocks func hasInstallPermission(caller address, position string) bool { if isPersonalWorld(position) { worldID := extractPersonalWorldID(position) return personalworld.HasInstallPermission(worldID, caller) } if isSystemChunk(position) { tid := grc721.TokenID(extractSystemChunkID(position)) return chunk.HasInstallPermission(tid, caller) } return false } // hasUninstallPermission checks if caller has world permission to uninstall blocks func hasUninstallPermission(caller address, position string) bool { if isPersonalWorld(position) { worldID := extractPersonalWorldID(position) return personalworld.HasUninstallPermission(worldID, caller) } if isSystemChunk(position) { tid := grc721.TokenID(extractSystemChunkID(position)) return chunk.HasUninstallPermission(tid, caller) } return false }