fish.gno
13.03 Kb · 626 lines
1// Package fish is the Shoal collection. Each fish is a GRC721 on this realm.
2// List, cancel, buy, and Bite live here too. Gno cannot pull a foreign token into
3// Bazaar, so the book is not a second package.
4//
5// Not deployed. Init does not call bazaarv2 Reserve or Register.
6package fish
7
8import (
9 "chain"
10 "chain/banker"
11 "chain/runtime/unsafe"
12 "errors"
13 "strconv"
14 "strings"
15
16 "gno.land/p/nt/grc721/metadata/v0"
17 "gno.land/p/nt/grc721/v0"
18 "gno.land/p/nt/seqid/v0"
19)
20
21const (
22 denom = "ugnot"
23 ugnot = 1_000_000
24 mintFee = 5 * ugnot
25 layFee = 2 * ugnot
26 upgradeFee = 15 * ugnot
27 protocolBps = 200
28 maxRoyalty = 1000
29 maxPrice = 1_000_000 * ugnot
30 combatLockMs = 5000
31 layBlocks = 2
32 listCap = 50
33)
34
35type fishBody struct {
36 id int
37 name string
38 rarity string
39 species string
40 size string
41 trait string
42 hp int
43 hpNow int
44 dodge int
45 bouts int
46 layBoutsUsed int
47 layOpen bool
48 layHeight int64
49 pool int64
50 inMatch bool
51 boutPaid bool
52 mustHeal bool
53 startUnix int64
54 endUnix int64
55 sx int
56 sz int
57 faceDx int
58 faceDz int
59 growth int
60 evolved bool
61 aspect string
62 aspectBps int
63 healDay int64
64 healsUsed int
65 healReadyAt int64
66 springBorn int64
67 nextMoveMilli int64
68 burstUntil int64
69 nextBurstAt int64
70 nextBiteMilli int64
71 lastCombatMilli int64
72 bittenBy int
73 bittenUntil int64
74 owner address
75 seller address
76 price int64
77 listed bool
78 burned bool
79 image string
80}
81
82var (
83 bodies []*fishBody
84 nextID int
85 minted int
86 inited bool
87 token *grc721.Token
88 ledger *grc721.PrivateLedger
89 metaView *metadata.Metadata
90 metaLed *metadata.Ledger
91 tokenSeq seqid.ID
92 creator address
93 self address
94 upgradeSink address
95 lpSink address
96 burnSink address
97 protocolSink address
98 seasonSink address
99 royaltyBps int64
100 imageBase string
101 colName string
102 colSymbol string
103)
104
105func Init(cur realm, name, symbol, images string, royalty int64, upgrade, lp, burn, protocol, season address) {
106 if !cur.IsCurrent() {
107 panic("fish: realm")
108 }
109 if !cur.Previous().IsUserCall() {
110 panic("fish: eoa")
111 }
112 if inited {
113 panic("fish: init")
114 }
115 takeExact(0)
116 if !validCollectionName(name) || !validSymbol(symbol) {
117 panic("fish: name")
118 }
119 if !strings.HasPrefix(images, "https://") || strings.Contains(images, "|") || len(images) > 160 {
120 panic("fish: image")
121 }
122 if royalty < 0 || royalty > maxRoyalty {
123 panic("fish: royalty")
124 }
125 if !upgrade.IsValid() || !lp.IsValid() || !burn.IsValid() || !protocol.IsValid() || !season.IsValid() {
126 panic("fish: sink")
127 }
128 tok, led := grc721.NewToken(name, symbol, tokenSeq.Next(), cur)
129 token = tok
130 ledger = led
131 metaView, metaLed = metadata.NewMetadata(led)
132 creator = cur.Previous().Address()
133 self = cur.Address()
134 upgradeSink = upgrade
135 lpSink = lp
136 burnSink = burn
137 protocolSink = protocol
138 seasonSink = season
139 royaltyBps = royalty
140 imageBase = images
141 colName = name
142 colSymbol = symbol
143 bodies = []*fishBody{nil}
144 nextID = 0
145 minted = 0
146 inited = true
147 chain.Emit("Init", "creator", creator.String(), "symbol", symbol)
148}
149
150func Mint(cur realm, name, size, trait string) int {
151 caller := requireUser(cur)
152 if unplayed(caller) {
153 panic("fish: unplayed")
154 }
155 if !validFishName(name) {
156 panic("fish: name")
157 }
158 if !validSize(size) {
159 panic("fish: size")
160 }
161 if !validTrait(trait) {
162 panic("fish: trait")
163 }
164 takeExact(mintFee)
165 payMint(mintFee, cur)
166 return issue(caller, name, "common", size, trait)
167}
168
169func Transfer(cur realm, to address, id int) {
170 caller := requireUser(cur)
171 takeExact(0)
172 if !to.IsValid() || to == caller || to == self {
173 panic("fish: addr")
174 }
175 body := must(id)
176 if body.owner != caller {
177 panic("fish: owner")
178 }
179 guardMovable(body)
180 moveToken(caller, caller, to, id)
181 body.owner = to
182}
183
184func TransferFrom(cur realm, from, to, tid string) {
185 caller := requireUser(cur)
186 takeExact(0)
187 id := parseID(tid)
188 src := address(from)
189 dst := address(to)
190 if !src.IsValid() || !dst.IsValid() || dst == self {
191 panic("fish: addr")
192 }
193 body := must(id)
194 if body.owner != src {
195 panic("fish: owner")
196 }
197 guardMovable(body)
198 moveToken(caller, src, dst, id)
199 body.owner = dst
200}
201
202func Approve(cur realm, to, tid string) {
203 caller := requireUser(cur)
204 takeExact(0)
205 id := parseID(tid)
206 body := must(id)
207 guardMovable(body)
208 who := address(to)
209 if to != "" && !who.IsValid() {
210 panic("fish: addr")
211 }
212 if err := ledger.Approve(caller, who, tokenID(id)); err != nil {
213 panic("fish: approve")
214 }
215}
216
217func SetApprovalForAll(cur realm, operator string, approved bool) {
218 caller := requireUser(cur)
219 takeExact(0)
220 op := address(operator)
221 if !op.IsValid() {
222 panic("fish: addr")
223 }
224 if err := ledger.SetApprovalForAll(caller, op, approved); err != nil {
225 panic("fish: approve")
226 }
227}
228
229func OwnerOf(tid string) (address, error) {
230 requireInit()
231 id, err := strconv.Atoi(tid)
232 if err != nil || id < 1 || id >= len(bodies) || bodies[id] == nil || bodies[id].burned {
233 return address(""), errors.New("fish: unknown")
234 }
235 return bodies[id].owner, nil
236}
237
238func Name() string {
239 requireInit()
240 return token.GetName()
241}
242
243func Symbol() string {
244 requireInit()
245 return token.GetSymbol()
246}
247
248func GrcName() string { return Name() }
249func GrcSymbol() string { return Symbol() }
250
251func BalanceOf(addr string) int64 {
252 requireInit()
253 who := address(addr)
254 if !who.IsValid() {
255 return 0
256 }
257 n, err := token.BalanceOf(who)
258 if err != nil {
259 return 0
260 }
261 return n
262}
263
264func GrcBalanceOf(addr string) int64 { return BalanceOf(addr) }
265
266func TokenURI(tid string) string {
267 id := parseID(tid)
268 must(id)
269 uri, err := metaView.TokenURI(tokenID(id))
270 if err != nil || uri == "" {
271 panic("fish: uri")
272 }
273 return uri
274}
275
276func TokensOf(owner string) string {
277 requireInit()
278 who := address(owner)
279 if !who.IsValid() {
280 return ""
281 }
282 out := ""
283 for _, body := range bodies {
284 if body == nil || body.burned || body.owner != who {
285 continue
286 }
287 if out != "" {
288 out += ","
289 }
290 out += strconv.Itoa(body.id)
291 }
292 return out
293}
294
295func TokenOfOwnerByIndex(owner string, index int64) string {
296 requireInit()
297 who := address(owner)
298 if !who.IsValid() {
299 panic("fish: addr")
300 }
301 if index < 0 {
302 panic("fish: index")
303 }
304 var seen int64
305 for _, body := range bodies {
306 if body == nil || body.burned || body.owner != who {
307 continue
308 }
309 if seen == index {
310 return strconv.Itoa(body.id)
311 }
312 seen++
313 }
314 panic("fish: index")
315}
316
317func TotalSupply() int64 {
318 requireInit()
319 return token.TotalSupply()
320}
321
322func GetApproved(id int) string {
323 requireInit()
324 who, err := token.GetApproved(tokenID(id))
325 if err != nil || !who.IsValid() {
326 return ""
327 }
328 return who.String()
329}
330
331func IsApprovedForAll(owner, operator string) bool {
332 requireInit()
333 a := address(owner)
334 b := address(operator)
335 if !a.IsValid() || !b.IsValid() {
336 return false
337 }
338 return token.IsApprovedForAll(a, b)
339}
340
341func MintPrice() int64 { return mintFee }
342func MaxSupply() int { return 0 }
343func Minted() int { return minted }
344func RoyaltyBps() int64 { return royaltyBps }
345func Creator() address { return creator }
346func ProtocolBps() int64 { return protocolBps }
347
348func BoutsOf(id int) int { return must(id).bouts }
349func PoolOf(id int) int64 { return must(id).pool }
350func Listed(id int) bool { return must(id).listed }
351func InMatch(id int) bool { return must(id).inMatch }
352func LayOpen(id int) bool { return must(id).layOpen }
353func RarityOf(id int) string { return must(id).rarity }
354func SizeOf(id int) string { return must(id).size }
355func TraitOf(id int) string { return must(id).trait }
356func SpeciesOf(id int) string { return must(id).species }
357
358func issue(to address, name, rarity, size, trait string) int {
359 nextID++
360 id := nextID
361 maxHP := hpOf(rarity, size, trait)
362 body := &fishBody{
363 id: id,
364 name: name,
365 rarity: rarity,
366 species: speciesOf(rarity),
367 size: size,
368 trait: trait,
369 hp: maxHP,
370 hpNow: maxHP,
371 dodge: dodgeOf(size),
372 faceDx: 0,
373 faceDz: -1,
374 aspectBps: 10000,
375 springBorn: -1,
376 lastCombatMilli: -1,
377 owner: to,
378 image: imageBase + "/" + rarity + ".png",
379 }
380 for len(bodies) <= id {
381 bodies = append(bodies, nil)
382 }
383 bodies[id] = body
384 minted++
385 mintGRC(to, body)
386 return id
387}
388
389func mintGRC(to address, body *fishBody) {
390 tid := tokenID(body.id)
391 if err := ledger.Mint(to, tid); err != nil {
392 panic("fish: mint")
393 }
394 if err := metaLed.SetTokenURI(tid, tokenJSON(body)); err != nil {
395 panic("fish: uri")
396 }
397}
398
399func burnGRC(id int) {
400 if err := ledger.Burn(tokenID(id)); err != nil {
401 panic("fish: burn")
402 }
403}
404
405func moveToken(spender, from, to address, id int) {
406 if err := ledger.TransferFrom(spender, from, to, tokenID(id)); err != nil {
407 panic("fish: transfer")
408 }
409}
410
411func tokenJSON(body *fishBody) string {
412 return "data:application/json;charset=utf-8," +
413 `{"name":"` + body.name + `","description":"Shoal fish","image":"` + body.image +
414 `","attributes":[{"trait_type":"Species","value":"` + body.species +
415 `"},{"trait_type":"Size","value":"` + body.size +
416 `"},{"trait_type":"Trait","value":"` + body.trait +
417 `"},{"trait_type":"Rarity","value":"` + body.rarity +
418 `"},{"trait_type":"HP","value":"` + strconv.Itoa(body.hp) + `"}]}`
419}
420
421func requireUser(cur realm) address {
422 if !cur.IsCurrent() {
423 panic("fish: realm")
424 }
425 if !cur.Previous().IsUserCall() {
426 panic("fish: eoa")
427 }
428 return cur.Previous().Address()
429}
430
431func requireInit() {
432 if !inited || token == nil {
433 panic("fish: init")
434 }
435}
436
437func takeExact(n int64) {
438 sent := unsafe.OriginSend()
439 if n == 0 {
440 if len(sent) != 0 {
441 panic("fish: payment")
442 }
443 return
444 }
445 if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount != n {
446 panic("fish: payment")
447 }
448}
449
450func payMint(fee int64, rlm realm) {
451 up := fee * 7000 / 10000
452 lp := fee * 2000 / 10000
453 burn := fee - up - lp
454 send(upgradeSink, up, rlm)
455 send(lpSink, lp, rlm)
456 send(burnSink, burn, rlm)
457}
458
459func send(to address, amt int64, rlm realm) {
460 if amt == 0 {
461 return
462 }
463 if !to.IsValid() {
464 panic("fish: sink")
465 }
466 bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)
467 bk.SendCoins(rlm.Address(), to, chain.Coins{{Denom: denom, Amount: amt}})
468}
469
470func must(id int) *fishBody {
471 requireInit()
472 if id < 1 || id >= len(bodies) || bodies[id] == nil || bodies[id].burned {
473 panic("fish: unknown")
474 }
475 return bodies[id]
476}
477
478func guardMovable(body *fishBody) {
479 if body.listed {
480 panic("fish: listed")
481 }
482 if body.inMatch {
483 panic("fish: match")
484 }
485 if body.pool != 0 {
486 panic("fish: pool")
487 }
488 if body.layOpen {
489 panic("fish: lay")
490 }
491}
492
493func unplayed(owner address) bool {
494 for _, body := range bodies {
495 if body == nil || body.burned || body.bouts >= 1 {
496 continue
497 }
498 if body.owner == owner || (body.listed && body.seller == owner) {
499 return true
500 }
501 }
502 return false
503}
504
505func tokenID(id int) grc721.TokenID {
506 return grc721.TokenID(strconv.Itoa(id))
507}
508
509func parseID(tid string) int {
510 id, err := strconv.Atoi(tid)
511 if err != nil || id < 1 {
512 panic("fish: unknown")
513 }
514 return id
515}
516
517func validFishName(name string) bool {
518 if len(name) < 1 || len(name) > 20 {
519 return false
520 }
521 for _, c := range name {
522 if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != ' ' {
523 return false
524 }
525 }
526 return true
527}
528
529func validCollectionName(name string) bool {
530 return name != "" && len(name) <= 64 && !strings.Contains(name, "|")
531}
532
533func validSymbol(symbol string) bool {
534 if symbol == "" || len(symbol) > grc721.MaxSymbolLen {
535 return false
536 }
537 for _, c := range symbol {
538 if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') {
539 return false
540 }
541 }
542 return true
543}
544
545func validSize(size string) bool {
546 return size == "small" || size == "medium" || size == "large"
547}
548
549func validTrait(trait string) bool {
550 return trait == "swift" || trait == "fang" || trait == "hide"
551}
552
553func speciesOf(rarity string) string {
554 switch rarity {
555 case "common":
556 return "Minnow"
557 case "uncommon":
558 return "Mackerel"
559 case "rare":
560 return "Snapper"
561 case "epic":
562 return "Angler"
563 case "legendary":
564 return "Koi Dragon"
565 case "mythic":
566 return "Abyss Leviathan"
567 default:
568 panic("fish: rarity")
569 }
570}
571
572func hpBase(rarity string) int {
573 switch rarity {
574 case "common":
575 return 100
576 case "uncommon":
577 return 112
578 case "rare":
579 return 126
580 case "epic":
581 return 142
582 case "legendary":
583 return 160
584 case "mythic":
585 return 180
586 default:
587 panic("fish: rarity")
588 }
589}
590
591func hpOf(rarity, size, trait string) int {
592 hp := hpBase(rarity)
593 if size == "small" {
594 hp = hp * 88 / 100
595 }
596 if size == "large" {
597 hp = hp * 120 / 100
598 }
599 if trait == "hide" {
600 hp = hp * 112 / 100
601 }
602 if hp < 1 {
603 hp = 1
604 }
605 return hp
606}
607
608func dodgeOf(size string) int {
609 if size == "small" {
610 return 24
611 }
612 return 0
613}
614
615func arenaStake(stake int64) bool {
616 if stake >= 5*ugnot && stake < 20*ugnot {
617 return true
618 }
619 if stake >= 20*ugnot && stake < 80*ugnot {
620 return true
621 }
622 if stake >= 80*ugnot && stake < 300*ugnot {
623 return true
624 }
625 return stake >= 300*ugnot && stake <= 1000*ugnot
626}