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}