Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

validation.gno

6.14 Kb · 250 lines
  1package craft
  2
  3import (
  4	"net/url"
  5	"strconv"
  6)
  7
  8const (
  9	recipeBatchLimit             = 100
 10	maxRecipeIngredients         = 16
 11	defaultRecipeListLimit       = 100
 12	defaultCraftFee        int64 = 1_000_000
 13)
 14
 15type craftIngredient struct {
 16	Slot    uint
 17	TokenID string
 18	Amount  int64
 19}
 20
 21func mustParseRecipeCSV(recipesCSV, itemIDsCSV string) ([]string, []uint32) {
 22	if recipesCSV == "" {
 23		panic("recipes is required")
 24	}
 25	if itemIDsCSV == "" {
 26		panic("item ids is required")
 27	}
 28
 29	canonicalRecipes := []string{}
 30	itemIDs := []uint32{}
 31	recipeStart, recipeEnd := 0, 0
 32	itemIDStart, itemIDEnd := 0, 0
 33	for {
 34		for recipeEnd < len(recipesCSV) && recipesCSV[recipeEnd] != ',' {
 35			recipeEnd++
 36		}
 37		for itemIDEnd < len(itemIDsCSV) && itemIDsCSV[itemIDEnd] != ',' {
 38			itemIDEnd++
 39		}
 40
 41		recipeToken := recipesCSV[recipeStart:recipeEnd]
 42		itemIDText := itemIDsCSV[itemIDStart:itemIDEnd]
 43		if recipeToken == "" {
 44			panic("empty recipe not allowed")
 45		}
 46		if itemIDText == "" {
 47			panic("empty item id not allowed")
 48		}
 49
 50		canonicalRecipe, _ := mustParseRecipe(recipeToken)
 51		itemIDValue, err := strconv.ParseUint(itemIDText, 10, 32)
 52		if err != nil {
 53			panic("invalid item id: " + itemIDText)
 54		}
 55
 56		canonicalRecipes = append(canonicalRecipes, canonicalRecipe)
 57		itemIDs = append(itemIDs, uint32(itemIDValue))
 58		if len(canonicalRecipes) > recipeBatchLimit {
 59			panic("recipes exceeds 100")
 60		}
 61
 62		recipeDone := recipeEnd >= len(recipesCSV)
 63		itemIDDone := itemIDEnd >= len(itemIDsCSV)
 64		if recipeDone != itemIDDone {
 65			panic("recipes and item ids count mismatch")
 66		}
 67		if recipeDone {
 68			return canonicalRecipes, itemIDs
 69		}
 70
 71		recipeEnd++
 72		itemIDEnd++
 73		recipeStart = recipeEnd
 74		itemIDStart = itemIDEnd
 75	}
 76}
 77
 78func mustParseRecipeKeyCSV(recipesCSV string) []string {
 79	if recipesCSV == "" {
 80		panic("recipes is required")
 81	}
 82
 83	canonicalRecipes := []string{}
 84	recipeStart, recipeEnd := 0, 0
 85	for {
 86		for recipeEnd < len(recipesCSV) && recipesCSV[recipeEnd] != ',' {
 87			recipeEnd++
 88		}
 89
 90		recipeToken := recipesCSV[recipeStart:recipeEnd]
 91		if recipeToken == "" {
 92			panic("empty recipe not allowed")
 93		}
 94
 95		canonicalRecipe, _ := mustParseRecipe(recipeToken)
 96		canonicalRecipes = append(canonicalRecipes, canonicalRecipe)
 97		if len(canonicalRecipes) > recipeBatchLimit {
 98			panic("recipes exceeds 100")
 99		}
100
101		if recipeEnd >= len(recipesCSV) {
102			return canonicalRecipes
103		}
104
105		recipeEnd++
106		recipeStart = recipeEnd
107	}
108}
109
110func mustParseRecipe(recipe string) (string, []craftIngredient) {
111	canonical, ingredients, errMsg := parseRecipe(recipe)
112	if errMsg != "" {
113		panic(errMsg)
114	}
115	return canonical, ingredients
116}
117
118// A valid recipe is "<4-hex occupancy>|<ingredient>|...". Occupancy bits
119// choose slots 0-15 in order; each ingredient is "tokenId:amount" or
120// "slot:tokenId:amount". Example: "0013|iron:1|iron:2|stick:2" (bits 0,1,4)
121// canonicalizes to "0013|0:iron:1|1:iron:2|4:stick:2".
122func parseRecipe(recipe string) (string, []craftIngredient, string) {
123	if len(recipe) < 7 || recipe[4] != '|' {
124		return "", nil, "invalid recipe"
125	}
126
127	occupancyValue, err := strconv.ParseUint(recipe[:4], 16, 16)
128	if err != nil || occupancyValue == 0 {
129		return "", nil, "invalid recipe occupancy"
130	}
131	occupancy := uint16(occupancyValue)
132	slots := occupiedSlots(occupancy)
133
134	ingredients := []craftIngredient{}
135	entryStart, entryEnd := 5, 5
136	for {
137		for entryEnd < len(recipe) && recipe[entryEnd] != '|' {
138			entryEnd++
139		}
140
141		if len(ingredients) == len(slots) {
142			return "", nil, "occupied slots and ingredients count mismatch"
143		}
144
145		ingredient, errMsg := parseRecipeIngredient(
146			recipe[entryStart:entryEnd],
147			slots[len(ingredients)],
148		)
149		if errMsg != "" {
150			return "", nil, errMsg
151		}
152		ingredients = append(ingredients, ingredient)
153		if len(ingredients) > maxRecipeIngredients {
154			return "", nil, "recipe ingredients exceeds 16"
155		}
156
157		if entryEnd >= len(recipe) {
158			break
159		}
160
161		entryEnd++
162		entryStart = entryEnd
163	}
164
165	if len(ingredients) != len(slots) {
166		return "", nil, "occupied slots and ingredients count mismatch"
167	}
168
169	canonicalRecipe := formatOccupancy(occupancy)
170	for _, ingredient := range ingredients {
171		canonicalRecipe += "|" +
172			strconv.FormatUint(uint64(ingredient.Slot), 10) + ":" +
173			url.PathEscape(ingredient.TokenID) + ":" +
174			strconv.FormatInt(ingredient.Amount, 10)
175	}
176	return canonicalRecipe, ingredients, ""
177}
178
179func parseRecipeIngredient(entry string, expectedSlot uint) (craftIngredient, string) {
180	amountSeparator := len(entry) - 1
181	for amountSeparator > 0 && entry[amountSeparator] != ':' {
182		amountSeparator--
183	}
184	if amountSeparator == 0 || entry[amountSeparator] != ':' {
185		return craftIngredient{}, "invalid recipe ingredient"
186	}
187
188	tokenField := entry[:amountSeparator]
189	amountText := entry[amountSeparator+1:]
190	encodedTokenID := tokenField
191	slotPrefixEnd := 0
192	for slotPrefixEnd < len(tokenField) && tokenField[slotPrefixEnd] != ':' {
193		slotPrefixEnd++
194	}
195	if slotPrefixEnd < len(tokenField) && tokenField[slotPrefixEnd] == ':' {
196		slotValue, err := strconv.ParseUint(tokenField[:slotPrefixEnd], 10, 8)
197		if err == nil {
198			if uint(slotValue) != expectedSlot {
199				return craftIngredient{}, "ingredient slot mismatch"
200			}
201			encodedTokenID = tokenField[slotPrefixEnd+1:]
202		}
203	}
204
205	tokenID, err := url.PathUnescape(encodedTokenID)
206	if err != nil || tokenID == "" {
207		return craftIngredient{}, "invalid item token id"
208	}
209
210	amount, err := strconv.ParseInt(amountText, 10, 64)
211	if err != nil {
212		return craftIngredient{}, "invalid ingredient amount: " + amountText
213	}
214	if amount <= 0 {
215		return craftIngredient{}, "ingredient amount must be positive"
216	}
217
218	return craftIngredient{
219		Slot:    expectedSlot,
220		TokenID: tokenID,
221		Amount:  amount,
222	}, ""
223}
224
225func occupiedSlots(occupancy uint16) []uint {
226	slots := []uint{}
227	for slot := uint(0); slot < 16; slot++ {
228		if occupancy&(uint16(1)<<slot) != 0 {
229			slots = append(slots, slot)
230		}
231	}
232	return slots
233}
234
235func formatOccupancy(occupancy uint16) string {
236	value := strconv.FormatUint(uint64(occupancy), 16)
237	return "0000"[:4-len(value)] + value
238}
239
240func assertRecipeListPageCount(page, count int) {
241	if page < 1 {
242		panic("page must be at least 1")
243	}
244	if count < 1 {
245		panic("count must be at least 1")
246	}
247	if count > listLimit {
248		panic("count exceeds listLimit (max: " + strconv.Itoa(listLimit) + ")")
249	}
250}