Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}