craft.gno
5.54 Kb · 221 lines
1package craft
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime/unsafe"
7 "strconv"
8
9 "gno.land/p/akkadia/beta/accesscontrol"
10 "gno.land/p/nt/bptree/v0"
11 "gno.land/r/akkadia/beta/admin"
12 "gno.land/r/akkadia/beta/equipment"
13 "gno.land/r/akkadia/beta/item"
14)
15
16const (
17 SetRecipeEvent = "SetRecipe"
18 DeleteRecipeEvent = "DeleteRecipe"
19 DeleteRecipesEvent = "DeleteRecipes"
20 SetListLimitEvent = "SetListLimit"
21 SetCraftFeeEvent = "SetCraftFee"
22 CraftEvent = "Craft"
23)
24
25var (
26 recipes = bptree.NewBPTree32()
27 listLimit = defaultRecipeListLimit
28 craftFee = defaultCraftFee
29)
30
31func init() { resetCraftState() }
32
33func resetCraftState() {
34 recipes = bptree.NewBPTree32()
35 listLimit = defaultRecipeListLimit
36 craftFee = defaultCraftFee
37}
38
39// SetRecipe stores the item definition ID produced by a canonical recipe.
40func SetRecipe(cur realm, recipe string, itemID uint32) {
41 accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
42 assertCraftNoCoins()
43 canonicalRecipe, _ := mustParseRecipe(recipe)
44 setRecipe(canonicalRecipe, itemID)
45}
46
47// SetRecipes stores up to 100 canonical recipe and item ID pairs in one transaction.
48func SetRecipes(cur realm, recipesCSV, itemIDsCSV string) {
49 accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
50 assertCraftNoCoins()
51 canonicalRecipes, itemIDs := mustParseRecipeCSV(recipesCSV, itemIDsCSV)
52 for i, recipe := range canonicalRecipes {
53 setRecipe(recipe, itemIDs[i])
54 }
55}
56
57func setRecipe(canonicalRecipe string, itemID uint32) {
58 recipes.Set(canonicalRecipe, itemID)
59 chain.Emit(
60 SetRecipeEvent,
61 "recipe", canonicalRecipe,
62 "itemId", strconv.FormatUint(uint64(itemID), 10),
63 )
64}
65
66func DeleteRecipe(cur realm, recipe string) {
67 accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
68 assertCraftNoCoins()
69 canonicalRecipe := mustCanonicalRecipe(recipe)
70 deleteRecipe(canonicalRecipe)
71 chain.Emit(DeleteRecipeEvent, "recipe", canonicalRecipe)
72}
73
74func DeleteRecipes(cur realm, recipesCSV string) {
75 accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
76 assertCraftNoCoins()
77 canonicalRecipes := mustParseRecipeKeyCSV(recipesCSV)
78 for _, recipe := range canonicalRecipes {
79 if !recipes.Has(recipe) {
80 panic("recipe not found: " + recipe)
81 }
82 }
83 for _, recipe := range canonicalRecipes {
84 deleteRecipe(recipe)
85 }
86 chain.Emit(DeleteRecipesEvent, "count", strconv.Itoa(len(canonicalRecipes)))
87}
88
89func deleteRecipe(canonicalRecipe string) {
90 if _, removed := recipes.Remove(canonicalRecipe); !removed {
91 panic("recipe not found: " + canonicalRecipe)
92 }
93}
94
95func HasRecipe(recipe string) bool {
96 canonical, _, errMsg := parseRecipe(recipe)
97 if errMsg != "" {
98 return false
99 }
100 return recipes.Has(canonical)
101}
102
103func GetRecipe(recipe string) uint32 {
104 return mustGetRecipeItemID(mustCanonicalRecipe(recipe))
105}
106
107func GetRecipeCount() int {
108 return recipes.Size()
109}
110
111func ListRecipes(page, count int) []map[string]string {
112 assertRecipeListPageCount(page, count)
113 result := []map[string]string{}
114 recipes.IterateByOffset((page-1)*count, count, func(recipe string, value any) bool {
115 result = append(result, map[string]string{
116 "recipe": recipe,
117 "itemId": strconv.FormatUint(uint64(value.(uint32)), 10),
118 })
119 return false
120 })
121 return result
122}
123
124func SetListLimit(cur realm, limit int) {
125 accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
126 assertCraftNoCoins()
127 if limit < 1 {
128 panic("limit must be at least 1")
129 }
130
131 oldLimit := listLimit
132 listLimit = limit
133 chain.Emit(
134 SetListLimitEvent,
135 "oldLimit", strconv.Itoa(oldLimit),
136 "newLimit", strconv.Itoa(limit),
137 )
138}
139
140func GetListLimit() int {
141 return listLimit
142}
143
144func SetCraftFee(cur realm, fee int64) {
145 accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
146 assertCraftNoCoins()
147 if fee < 0 {
148 panic("fee must be non-negative")
149 }
150
151 oldFee := craftFee
152 craftFee = fee
153 chain.Emit(
154 SetCraftFeeEvent,
155 "oldFee", strconv.FormatInt(oldFee, 10),
156 "newFee", strconv.FormatInt(fee, 10),
157 )
158}
159
160func GetCraftFee() int64 {
161 return craftFee
162}
163
164// Craft burns a canonical recipe and mints the next GRC721 equipment token.
165func Craft(cur realm, recipe string) uint64 {
166 caller := accesscontrol.MustGetUserCaller(0, cur)
167 canonicalRecipe, ingredients := mustParseRecipe(recipe)
168 itemID := mustGetRecipeItemID(canonicalRecipe)
169 assertExactUgnotPayment(craftFee)
170
171 for _, ingredient := range ingredients {
172 item.BurnFromRealm(cross(cur), caller, ingredient.TokenID, ingredient.Amount)
173 }
174 tokenID := equipment.MintFromRealm(cross(cur), caller, itemID, "", "")
175 if craftFee > 0 {
176 bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
177 bnk.SendCoins(cur.Address(), admin.GetFeeCollector(), chain.Coins{{"ugnot", craftFee}})
178 }
179
180 chain.Emit(
181 CraftEvent,
182 "owner", caller.String(),
183 "recipe", canonicalRecipe,
184 "equipmentTokenId", strconv.FormatUint(tokenID, 10),
185 "itemId", strconv.FormatUint(uint64(itemID), 10),
186 "fee", strconv.FormatInt(craftFee, 10),
187 )
188 return tokenID
189}
190
191func mustCanonicalRecipe(recipe string) string {
192 canonical, _ := mustParseRecipe(recipe)
193 return canonical
194}
195
196func mustGetRecipeItemID(recipe string) uint32 {
197 value := recipes.Get(recipe)
198 if value == nil {
199 panic("recipe not found: " + recipe)
200 }
201 return value.(uint32)
202}
203
204func assertCraftNoCoins() {
205 if len(unsafe.OriginSend()) != 0 {
206 panic("attached coins not allowed")
207 }
208}
209
210func assertExactUgnotPayment(expected int64) {
211 coins := unsafe.OriginSend()
212 if expected == 0 {
213 if len(coins) != 0 {
214 panic("exact payment required: expected no coins, got " + coins.String())
215 }
216 return
217 }
218 if len(coins) != 1 || coins[0].Denom != "ugnot" || coins[0].Amount != expected {
219 panic("exact payment required: expected " + strconv.FormatInt(expected, 10) + "ugnot, got " + coins.String())
220 }
221}