Search Apps Documentation Source Content File Folder Download Copy Actions Download

action.gno

8.46 Kb ยท 350 lines
  1package block
  2
  3import (
  4	"strconv"
  5
  6	"chain"
  7	"chain/banker"
  8	"chain/runtime"
  9
 10	"gno.land/p/demo/tokens/grc721"
 11	"gno.land/p/nt/avl"
 12	"gno.land/p/nt/ufmt"
 13
 14	"gno.land/r/akkadia/admin"
 15	"gno.land/r/akkadia/chunk"
 16	personalworld "gno.land/r/akkadia/personal_world"
 17)
 18
 19const (
 20	BlockInstalledEvent   = "BlockInstalled"
 21	BlockUninstalledEvent = "BlockUninstalled"
 22	BlockUsedEvent        = "BlockUsed"
 23)
 24
 25var (
 26	nextLogID    uint64 = 1
 27	blockUseLogs = avl.NewTree() // logID -> map[string]string (position, tid, user, installer, fee, etc)
 28	userLogIndex = avl.NewTree() // user -> []uint64 (logIDs)
 29)
 30
 31// Install installs a block at position with handler integration
 32func Install(cur realm, position string, blockID uint32, content string, option string) {
 33	caller := runtime.PreviousRealm().Address()
 34
 35	// 1. Check world permission
 36	if !hasInstallPermission(caller, position) {
 37		panic("unauthorized: no permission to install block in this world")
 38	}
 39
 40	// 2. Check if already installed
 41	if _, exists := getInstalled(position); exists {
 42		panic("block already installed at position")
 43	}
 44
 45	// 3. Get block metadata
 46	blockIDStr := blockIDToString(blockID)
 47	blockIDKey := blockIDToKey(blockID)
 48	blockVal, found := blocks.Get(blockIDKey)
 49	if !found {
 50		panic("block not found: " + blockIDStr)
 51	}
 52	block := blockVal.(map[string]string)
 53	tokenID := blockIDToTokenID(blockID)
 54
 55	// 4. Get GRC1155 balance
 56	balance := BalanceOf(caller, tokenID)
 57	if balance < 1 {
 58		panic("insufficient block balance: block " + blockIDStr + " requires at least 1")
 59	}
 60
 61	// 5. Burn GRC1155
 62	burn(caller, tokenID, 1)
 63
 64	// 6. Save installed block
 65	shape := block["shape"]
 66	state := block["state"]
 67	saveInstalled(position, blockID, content, option, caller, shape, state)
 68
 69	// 7. Emit event
 70	chain.Emit(BlockInstalledEvent,
 71		"position", position,
 72		"blockId", blockIDStr,
 73		"content", content,
 74		"option", option,
 75		"installer", caller.String(),
 76	)
 77}
 78
 79// Uninstall uninstalls a block at position
 80func Uninstall(cur realm, position string) {
 81	caller := runtime.PreviousRealm().Address()
 82
 83	// 1. Check world permission
 84	if !hasUninstallPermission(caller, position) {
 85		panic("unauthorized: no permission to uninstall block in this world")
 86	}
 87
 88	// 2. Check if block exists
 89	mustGetInstalled(position)
 90
 91	// 3. Remove from storage
 92	removeInstalled(position)
 93
 94	// 4. Emit event
 95	chain.Emit(BlockUninstalledEvent,
 96		"position", position,
 97	)
 98}
 99
100// Read reads block data at position (query-only)
101func Read(position string) string {
102	// Get installed block
103	info := mustGetInstalled(position)
104
105	// Return content field
106	return info["content"]
107}
108
109// Use uses a block at position and updates its option
110func Use(cur realm, position string) {
111	caller := runtime.PreviousRealm().Address()
112
113	// 1. Get installed block
114	info := mustGetInstalled(position)
115
116	// 2. Get block tid
117	blockIDStr := info["blockId"]
118	blockID := stringToBlockID(blockIDStr)
119	blockKey := blockIDToKey(blockID)
120
121	// 3. Get block properties
122	blockVal, found := blocks.Get(blockKey)
123	if !found {
124		panic("block not found: " + blockIDStr)
125	}
126	block := blockVal.(map[string]string)
127
128	// 4. Get usePrice and installerBps from block
129	usePriceStr, found := block["usePrice"]
130	if !found {
131		panic("usePrice not found in block")
132	}
133	usePrice, err := strconv.ParseInt(usePriceStr, 10, 64)
134	if err != nil {
135		panic("invalid usePrice: " + err.Error())
136	}
137
138	installerBpsStr, found := block["installerBps"]
139	if !found {
140		panic("installerBps not found in block")
141	}
142	installerBps, err := strconv.Atoi(installerBpsStr)
143	if err != nil {
144		panic("invalid installerBps: " + err.Error())
145	}
146
147	// 5. Check payment
148	sent := banker.OriginSend()
149	paidAmount := sent.AmountOf("ugnot")
150	if paidAmount < usePrice {
151		panic("insufficient payment: required " + ufmt.Sprintf("%d", usePrice) + " ugnot")
152	}
153
154	// 6. Get installer
155	installer := address(info["installer"])
156
157	// 7. Distribute funds
158	installerShare, operatorShare := distributeFunds(installer, usePrice, installerBps)
159
160	// 8. Refund excess
161	if paidAmount > usePrice {
162		bnk := banker.NewBanker(banker.BankerTypeRealmSend)
163		realmAddr := runtime.CurrentRealm().Address()
164		refund := paidAmount - usePrice
165		coins := chain.Coins{chain.Coin{"ugnot", refund}}
166		bnk.SendCoins(realmAddr, caller, coins)
167	}
168
169	// 9. Create use log
170	logID := getNextLogID()
171	logIDStr := logIDToString(logID)
172	log := map[string]string{
173		"id":             logIDStr,
174		"position":       position,
175		"blockId":        blockIDStr,
176		"user":           caller.String(),
177		"installer":      installer.String(),
178		"fee":            ufmt.Sprintf("%d", usePrice),
179		"installerShare": ufmt.Sprintf("%d", installerShare),
180		"operatorShare":  ufmt.Sprintf("%d", operatorShare),
181		"height":         ufmt.Sprintf("%d", runtime.ChainHeight()),
182	}
183
184	blockUseLogs.Set(logIDStr, log)
185
186	// 10. Update user log index
187	addUserLogIndex(caller, logID)
188
189	// 11. Emit event
190	chain.Emit(BlockUsedEvent,
191		"logId", logIDStr,
192		"position", position,
193		"blockId", blockIDStr,
194		"user", caller.String(),
195		"fee", ufmt.Sprintf("%d", usePrice),
196	)
197}
198
199// addUserLogIndex adds a log entry to user's log index
200func addUserLogIndex(user address, logID uint64) {
201	userStr := user.String()
202	logs, found := userLogIndex.Get(userStr)
203
204	var logIDs []uint64
205	if found {
206		logIDs = logs.([]uint64)
207	}
208
209	logIDs = append(logIDs, logID)
210	userLogIndex.Set(userStr, logIDs)
211}
212
213func distributeFunds(installer address, amount int64, installerBPS int) (int64, int64) {
214	// Calculate shares (BPS)
215	installerShare := (amount * int64(installerBPS)) / 10000
216	operatorShare := amount - installerShare
217
218	// Send funds to operator and protocol
219	bnk := banker.NewBanker(banker.BankerTypeRealmSend)
220	realmAddr := runtime.CurrentRealm().Address()
221
222	if installerShare > 0 {
223		coins := chain.Coins{chain.Coin{"ugnot", installerShare}}
224		bnk.SendCoins(realmAddr, installer, coins)
225	}
226
227	if operatorShare > 0 {
228		coins := chain.Coins{chain.Coin{"ugnot", operatorShare}}
229		bnk.SendCoins(realmAddr, admin.GetFeeCollector(), coins)
230	}
231
232	return installerShare, operatorShare
233}
234
235// ListInstalled returns installed blocks for given positions
236// position format: "{type}|{worldId}|{owner}|{xIndex}|{zIndex}|{blockIndex}"
237func ListInstalled(positions ...string) []map[string]string {
238	result := []map[string]string{}
239
240	for _, position := range positions {
241		store := getStore(position)
242		storeKey, chunkCoord, blockIndex := parsePosition(position)
243
244		world, found := store.Get(storeKey)
245		if !found {
246			continue
247		}
248
249		worldMap := world.(map[string]map[string]map[string]string)
250		chunkMap, found := worldMap[chunkCoord]
251		if !found {
252			continue
253		}
254
255		info, found := chunkMap[blockIndex]
256		if !found {
257			continue
258		}
259
260		result = append(result, info)
261	}
262
263	return result
264}
265
266// ListUseLogs returns user's block use logs (newest first, limited)
267func ListUseLogs(user address, limit int) []map[string]string {
268	result := []map[string]string{}
269
270	if limit <= 0 {
271		return result
272	}
273
274	userStr := user.String()
275	logs, found := userLogIndex.Get(userStr)
276	if !found {
277		return result
278	}
279
280	logIDs := logs.([]uint64)
281
282	// Reverse iterate (newest first)
283	count := 0
284	for i := len(logIDs) - 1; i >= 0; i-- {
285		if count >= limit {
286			break
287		}
288
289		logID := logIDs[i]
290
291		// Get log details from blockUseLogs
292		logIDKey := logIDToString(logID)
293		logData, found := blockUseLogs.Get(logIDKey)
294		if !found {
295			continue
296		}
297
298		log := logData.(map[string]string)
299		result = append(result, log)
300		count++
301	}
302
303	return result
304}
305
306func getNextLogID() uint64 {
307	id := nextLogID
308	nextLogID++
309	return id
310}
311
312func logIDToString(logID uint64) string {
313	return ufmt.Sprintf("%d", logID)
314}
315
316// HasInstalled checks if a block is installed at position
317func HasInstalled(position string) bool {
318	_, exists := getInstalled(position)
319	return exists
320}
321
322// hasInstallPermission checks if caller has world permission to install blocks
323func hasInstallPermission(caller address, position string) bool {
324	if isPersonalWorld(position) {
325		worldID := extractPersonalWorldID(position)
326		return personalworld.HasInstallPermission(worldID, caller)
327	}
328
329	if isSystemChunk(position) {
330		tid := grc721.TokenID(extractSystemChunkID(position))
331		return chunk.HasInstallPermission(tid, caller)
332	}
333
334	return false
335}
336
337// hasUninstallPermission checks if caller has world permission to uninstall blocks
338func hasUninstallPermission(caller address, position string) bool {
339	if isPersonalWorld(position) {
340		worldID := extractPersonalWorldID(position)
341		return personalworld.HasUninstallPermission(worldID, caller)
342	}
343
344	if isSystemChunk(position) {
345		tid := grc721.TokenID(extractSystemChunkID(position))
346		return chunk.HasUninstallPermission(tid, caller)
347	}
348
349	return false
350}