action.gno
8.43 Kb · 260 lines
1package block
2
3import (
4 "strconv"
5 "strings"
6
7 "chain"
8 "chain/runtime/unsafe"
9
10 "gno.land/p/akkadia/v0/accesscontrol"
11 "gno.land/p/akkadia/v0/grc721"
12
13 "gno.land/r/akkadia/v0/admin"
14 "gno.land/r/akkadia/v0/chunk"
15 personalworld "gno.land/r/akkadia/v0/personal_world"
16)
17
18const (
19 BlockInstalledEvent = "BlockInstalled"
20 BlockUninstalledEvent = "BlockUninstalled"
21 BlockUsedEvent = "BlockUsed"
22)
23
24func Install(cur realm, position string, blockID uint32, propKeys string, propValues string) {
25 assertNotFrozen()
26 caller := accesscontrol.MustGetUserCaller(0, cur)
27 positionType, storeKey, coordKey, blockIndexKey := parsePosition(position)
28
29 props := parseOptPropsCSV(propKeys, propValues)
30 installValidator.MustValidate(props)
31
32 if !hasInstallPermission(caller, positionType, storeKey, coordKey) {
33 panic("unauthorized: no permission to install block in this world")
34 }
35 if installedBlockStore.HasInstalled(positionType, storeKey, coordKey, blockIndexKey) {
36 panic("block already installed at position")
37 }
38
39 block := blockStore.MustGet(blockID)
40 tokenID := blockIDToTokenID(blockID)
41 if mintedBlockStore.BalanceOf(caller, tokenID) < 1 {
42 panic("insufficient block balance: block " + string(tokenID) + " requires at least 1")
43 }
44
45 // installValidator rejects these keys before canonical block-owned fields
46 // are injected, so clients cannot override identity, installer, shape, or state.
47 props["blockId"] = string(tokenID)
48 props["installer"] = caller.String()
49 props["shape"] = block["shape"]
50 props["state"] = block["state"]
51
52 mintedBlockStore.Burn(caller, caller, tokenID, 1)
53 installedBlockStore.SaveInstalled(positionType, storeKey, coordKey, blockIndexKey, props)
54
55 chain.Emit(BlockInstalledEvent,
56 "position", position,
57 "blockId", props["blockId"],
58 "installer", props["installer"],
59 )
60}
61
62func Uninstall(cur realm, position string) {
63 assertNotFrozen()
64 caller := accesscontrol.MustGetUserCaller(0, cur)
65 positionType, storeKey, coordKey, blockIndexKey := parsePosition(position)
66
67 if !hasUninstallPermission(caller, positionType, storeKey, coordKey) {
68 panic("unauthorized: no permission to uninstall block in this world")
69 }
70 if _, found := installedBlockStore.GetInstalled(positionType, storeKey, coordKey, blockIndexKey); !found {
71 panic("block not found at position: " + position)
72 }
73
74 installedBlockStore.RemoveInstalled(positionType, storeKey, coordKey, blockIndexKey)
75
76 chain.Emit(BlockUninstalledEvent,
77 "position", position,
78 )
79}
80
81func Read(position string) string {
82 assertMigrationStateAvailable()
83 positionType, storeKey, coordKey, blockIndexKey := parsePosition(position)
84 info, found := installedBlockStore.GetInstalled(positionType, storeKey, coordKey, blockIndexKey)
85 if !found {
86 panic("block not found at position: " + position)
87 }
88 return info["content"]
89}
90
91func Use(cur realm, position string) {
92 assertNotFrozen()
93 caller := accesscontrol.MustGetUserCaller(0, cur)
94 positionType, storeKey, coordKey, blockIndexKey := parsePosition(position)
95
96 info, found := installedBlockStore.GetInstalled(positionType, storeKey, coordKey, blockIndexKey)
97 if !found {
98 panic("block not found at position: " + position)
99 }
100
101 blockID := stringToBlockID(info["blockId"])
102 block := blockStore.MustGet(blockID)
103 usePrice := int64(mustParseUint32(block["usePrice"]))
104 installerBPS := int(mustParseUint32(block["installerBps"]))
105
106 // MustGetUserCaller guarantees a direct user call, so OriginSend cannot be borrowed through an intermediate realm.
107 payment := unsafe.OriginSend().AmountOf("ugnot")
108 assertExactPayment(usePrice, payment)
109
110 feeCollector := admin.GetFeeCollector()
111 installerShare, feeCollectorShare := distributeShares(cur, info["installer"], feeCollector, payment, installerBPS)
112
113 useLog := map[string]string{
114 "position": position,
115 "blockId": info["blockId"],
116 "user": caller.String(),
117 "fee": block["usePrice"],
118 "installer": info["installer"],
119 "installerShare": strconv.FormatInt(installerShare, 10),
120 "feeCollector": feeCollector.String(),
121 "feeCollectorShare": strconv.FormatInt(feeCollectorShare, 10),
122 }
123
124 result := installedBlockStore.RecordUse(caller, useLog)
125
126 chain.Emit(BlockUsedEvent,
127 "logId", result["id"],
128 "position", position,
129 "blockId", useLog["blockId"],
130 "user", useLog["user"],
131 "fee", result["fee"],
132 )
133}
134
135// ListInstalled returns installed blocks for given positions
136// position format: "{type}|{worldId}|{owner}|{xIndex}|{zIndex}|{blockIndex}"
137func ListInstalled(positions ...string) []map[string]string {
138 assertMigrationStateAvailable()
139 result := []map[string]string{}
140 for _, position := range positions {
141 positionType, storeKey, coordKey, blockIndexKey := parsePosition(position)
142 if info, found := installedBlockStore.GetInstalled(positionType, storeKey, coordKey, blockIndexKey); found {
143 result = append(result, info)
144 }
145 }
146 return result
147}
148
149// ListUseLogs returns user's block use logs (newest first, limited)
150func ListUseLogs(user address, limit int) []map[string]string {
151 assertMigrationStateAvailable()
152 return installedBlockStore.ListUseLogs(user, limit)
153}
154
155func logIDToString(logID uint64) string {
156 return strconv.FormatUint(logID, 10)
157}
158
159// Position is the public install key:
160//
161// personal|{worldId}|{owner}|{xIndex}|{zIndex}|{blockIndex}
162// system|{chunkId}|{owner}|{xIndex}|{zIndex}|{blockIndex}
163//
164// Storage uses the world/chunk id as the top-level key, x/z as the coordinate
165// key, and blockIndex as the installed block key. Permission checks derive the
166// target world from the same string: personal uses worldId, system uses
167// chunkId:xIndex_zIndex. The owner segment is part of the client-facing key,
168// but it is not the authority source.
169// parsePosition parses position into storage key components.
170// Format: "type|worldId|owner|xIndex|zIndex|blockIndex"
171// Returns: (positionType, storeKey, coordKey, blockIndexKey)
172func parsePosition(position string) (positionType string, storeKey string, coordKey string, blockIndexKey string) {
173 parts := strings.Split(position, "|")
174 if len(parts) != 6 {
175 panic("invalid position format: must be 'type|worldId|owner|xIndex|zIndex|blockIndex'")
176 }
177
178 positionType = parts[0]
179 if positionType != "personal" && positionType != "system" {
180 panic("unknown position type: " + position)
181 }
182 storeKey = parts[1]
183 coordKey = parts[3] + "_" + parts[4]
184 blockIndexKey = parts[5]
185 return positionType, storeKey, coordKey, blockIndexKey
186}
187
188func isPersonalWorld(position string) bool {
189 return strings.HasPrefix(position, "personal|")
190}
191
192func isSystemChunk(position string) bool {
193 return strings.HasPrefix(position, "system|")
194}
195
196// extractPersonalWorldID expects: personal|{worldId}|{owner}|{xIndex}|{zIndex}|{blockIndex}
197func extractPersonalWorldID(position string) uint32 {
198 if !isPersonalWorld(position) {
199 panic("not a personal world position: " + position)
200 }
201
202 _, storeKey, _, _ := parsePosition(position)
203 id, err := strconv.ParseUint(storeKey, 10, 32)
204 if err != nil {
205 panic("invalid world ID: " + storeKey)
206 }
207 return uint32(id)
208}
209
210// extractSystemChunkID expects: system|{chunkId}|{owner}|{xIndex}|{zIndex}|{blockIndex}
211func extractSystemChunkID(position string) string {
212 if !isSystemChunk(position) {
213 panic("not a system chunk position: " + position)
214 }
215
216 _, storeKey, coordKey, _ := parsePosition(position)
217 return storeKey + ":" + coordKey
218}
219
220func HasInstalled(position string) bool {
221 assertMigrationStateAvailable()
222 positionType, storeKey, coordKey, blockIndexKey := parsePosition(position)
223 return installedBlockStore.HasInstalled(positionType, storeKey, coordKey, blockIndexKey)
224}
225
226func hasInstallPermission(caller address, positionType string, storeKey string, coordKey string) bool {
227 if positionType == "personal" {
228 worldID := mustParsePositionWorldID(storeKey)
229 return personalworld.HasInstallPermission(worldID, caller)
230 }
231
232 if positionType == "system" {
233 tokenID := grc721.TokenID(storeKey + ":" + coordKey)
234 return chunk.HasInstallPermission(tokenID, caller)
235 }
236
237 return false
238}
239
240func hasUninstallPermission(caller address, positionType string, storeKey string, coordKey string) bool {
241 if positionType == "personal" {
242 worldID := mustParsePositionWorldID(storeKey)
243 return personalworld.HasUninstallPermission(worldID, caller)
244 }
245
246 if positionType == "system" {
247 tokenID := grc721.TokenID(storeKey + ":" + coordKey)
248 return chunk.HasUninstallPermission(tokenID, caller)
249 }
250
251 return false
252}
253
254func mustParsePositionWorldID(storeKey string) uint32 {
255 id, err := strconv.ParseUint(storeKey, 10, 32)
256 if err != nil {
257 panic("invalid world ID: " + storeKey)
258 }
259 return uint32(id)
260}