seafish.gno
20.42 Kb · 739 lines
1// Package seafish is the tradable person-versus-person sea.
2// CommitMint pays 150 GNOT and the whole fee goes to the wallet that called Init.
3// RevealMint, once the chain is at least two blocks later, rolls rarity and trait
4// from sha256 of the caller, the commit height, that fixed height plus two, and the id.
5// Waiting longer does not change the roll. The player does not pass trait, size, or rarity.
6// The revealed fish is a GRC721 owned by that player. TokenURI is the https portrait.
7// Swim stays off chain. Register locks at least 10 GNOT. Leave returns it after 5 seconds.
8// Settle pays one covered stake: 80% winner, 10% liquidity, 10% team. Dust stays with the team.
9// There is no machine fish and no FISH draw. A seated, staked, or healing fish cannot be
10// listed or transferred. List, UpdatePrice, Cancel, and Buy are the Nomaq trade.
11// Buy pays the ask, in full, to the seller. RegisterCollection publishes the slug on bazaarv5.
12// fishsea, fishnft, gnofish, and fishlive stay as deployed. This package does not move them.
13// seafish-1. The mint cap is 10000. One wallet still seats only one fish.
14package seafish
15
16import (
17 "chain"
18 "chain/banker"
19 "chain/runtime"
20 "chain/runtime/unsafe"
21 "crypto/sha256"
22 "errors"
23 "strconv"
24 "time"
25
26 "gno.land/p/nt/grc721/enumerable/v0"
27 "gno.land/p/nt/grc721/v0"
28 "gno.land/p/nt/seqid/v0"
29 factory "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/bazaarv5"
30)
31
32const (
33 denom = "ugnot"
34 mintFee int64 = 150_000_000
35 stakeMin int64 = 10_000_000
36 quietMs int64 = 5_000
37 maxMint = 10_000
38 maxAsk int64 = 100_000_000_000_000
39 listCap = 50
40 slug = "seafish"
41 imageIPFS = "ipfs://bafybeifdmgbtq3nf7xuepvygotk6ulk4wo7xshsi2yr4xukglsdpzz7p2i/"
42 imageHTTPS = "https://gateway.pinata.cloud/ipfs/bafybeifdmgbtq3nf7xuepvygotk6ulk4wo7xshsi2yr4xukglsdpzz7p2i/"
43 cover = imageHTTPS + "common-fish.jpg"
44)
45
46var (
47 hpBase = [6]int{100, 122, 148, 178, 214, 256}
48 biteBase = [6]int{24, 30, 37, 45, 54, 66}
49 spdBase = [6]int{108, 104, 100, 96, 92, 88}
50 species = [6]string{"Bubble", "Tideblade", "Abysswing", "Embermaw", "Aurelion", "Astravex"}
51 rarities = [6]string{"common", "uncommon", "rare", "epic", "legendary", "mythic"}
52 portrait = [6]string{"common-fish.jpg", "uncommon-fish.jpg", "race-fish.jpg", "epic-fish.jpg", "legendary-fish.jpg", "mythic-fish.jpg"}
53 traits = [3]string{"swift", "fang", "hide"}
54)
55
56type fish struct {
57 owner address
58 name string
59 rarity string
60 trait string
61 file string
62 bite int
63 hpMax int
64 hp int
65 speed int
66 pool int64
67 bouts int
68 commitH int64
69 touched int64
70 inSea bool
71 revealed bool
72 mustHeal bool
73 seller address
74 ask int64
75 onSale bool
76}
77
78var (
79 inited bool
80 market bool
81 self address
82 mintTo address
83 lpSink address
84 team address
85 minted int
86 shown int
87 roster []fish
88 token *grc721.Token
89 ledger *grc721.PrivateLedger
90 teller grc721.Teller
91 indexed *enumerable.Enumerable
92 seq seqid.ID
93)
94
95// Init records the caller as the mint wallet, plus liquidity and team.
96// Those three must be distinct. Call once, from an EOA, with no coins.
97func Init(cur realm, lp, crew address) {
98 if !cur.Previous().IsUserCall() {
99 panic("seafish: eoa")
100 }
101 takeExact(0)
102 caller := cur.Previous().Address()
103 if inited || !caller.IsValid() || !lp.IsValid() || !crew.IsValid() || caller == lp || caller == crew || lp == crew {
104 panic("seafish: init")
105 }
106 inited = true
107 self = cur.Address()
108 mintTo = caller
109 lpSink = lp
110 team = crew
111 roster = []fish{{}}
112 tok, led := grc721.NewToken("GnoFish Sea", "GSEA", seq.Next(), cur)
113 token = tok
114 ledger = led
115 teller = led.CallerTeller()
116 indexed, _ = enumerable.NewEnumerable(led)
117 chain.Emit("Init", "mint", caller.String(), "lp", lp.String(), "team", crew.String())
118}
119
120// CommitMint sells one unrevealed fish for 150 GNOT. The fee leaves this realm in the same call.
121// The player owns the fish. The GRC721 is minted at RevealMint, when the portrait exists.
122func CommitMint(cur realm) int {
123 caller := gate(cur, mintFee)
124 if minted >= maxMint || pending(caller) != 0 {
125 panic("seafish: mint")
126 }
127 height := runtime.ChainHeight()
128 if height < 1 {
129 panic("seafish: mint")
130 }
131 id := grow()
132 roster[id].owner = caller
133 roster[id].commitH = height
134 payMint(mintFee, cur)
135 chain.Emit("Commit", "id", strconv.Itoa(id), "owner", caller.String())
136 return id
137}
138
139// RevealMint assigns rarity, trait, the species name, and the portrait.
140// Entropy is the commit height plus two. The NFT is minted to the caller.
141func RevealMint(cur realm, id int) {
142 caller := gate(cur, 0)
143 row := mustFish(id)
144 entropyH := row.commitH + 2
145 if row.owner != caller || row.revealed || runtime.ChainHeight() < entropyH {
146 panic("seafish: reveal")
147 }
148 rarity, trait, bite, hp, speed := Roll(caller, row.commitH, entropyH, id)
149 row.rarity = rarity
150 row.trait = trait
151 row.bite = bite
152 row.hpMax = hp
153 row.hp = hp
154 row.speed = speed
155 row.name = pickName(tierOf(rarity))
156 row.file = portrait[tierOf(rarity)]
157 row.revealed = true
158 if err := ledger.Mint(caller, tidOf(id)); err != nil {
159 panic("seafish: mint")
160 }
161 shown++
162 chain.Emit("Reveal", "id", strconv.Itoa(id), "rarity", rarity, "trait", trait, "name", row.name, "file", row.file)
163 noteSupply(cur)
164}
165
166// Register seats one revealed fish the caller still owns. The attached coins are the stake.
167// A listed fish, or a second fish from a wallet that is already seated, is refused.
168func Register(cur realm, id int, stake int64) {
169 caller := gate(cur, stake)
170 row := mustFish(id)
171 if stake < stakeMin || row.owner != caller || !row.revealed || row.inSea || row.mustHeal || row.onSale || row.pool != 0 || seated(caller) {
172 panic("seafish: register")
173 }
174 row.pool = stake
175 row.inSea = true
176 row.touched = time.Now().UnixMilli()
177 chain.Emit("Register", "id", strconv.Itoa(id), "stake", strconv.FormatInt(stake, 10))
178}
179
180// Leave returns the seated stake once 5 seconds have passed since Register.
181func Leave(cur realm, id int) {
182 caller := gate(cur, 0)
183 row := mustFish(id)
184 if row.owner != caller || !row.inSea || time.Now().UnixMilli() < row.touched+quietMs {
185 panic("seafish: leave")
186 }
187 stake := row.pool
188 row.pool = 0
189 row.inSea = false
190 send(caller, stake, cur)
191 chain.Emit("Leave", "id", strconv.Itoa(id), "stake", strconv.FormatInt(stake, 10))
192}
193
194// Settle pays the smaller stake. The NFT stays. HP falls to 30% and must heal.
195// The caller has to own the killer. This realm does not check who bit whom.
196func Settle(cur realm, id int, target int) {
197 caller := gate(cur, 0)
198 if id == target {
199 panic("seafish: settle")
200 }
201 atk := mustFish(id)
202 vic := mustFish(target)
203 if atk.owner != caller || !atk.inSea || !vic.inSea || atk.owner == vic.owner || atk.pool < stakeMin || vic.pool < stakeMin {
204 panic("seafish: settle")
205 }
206 covered := atk.pool
207 if vic.pool < covered {
208 covered = vic.pool
209 }
210 excess := vic.pool - covered
211 owner := vic.owner
212 vic.pool = 0
213 vic.inSea = false
214 vic.mustHeal = true
215 vic.hp = vic.hpMax * 30 / 100
216 if vic.hp < 1 {
217 vic.hp = 1
218 }
219 atk.bouts++
220 vic.bouts++
221 killer, lp, crew := splitKill(covered)
222 send(atk.owner, killer, cur)
223 send(lpSink, lp, cur)
224 send(team, crew, cur)
225 send(owner, excess, cur)
226 chain.Emit("Kill", "id", strconv.Itoa(id), "target", strconv.Itoa(target), "covered", strconv.FormatInt(covered, 10))
227}
228
229// Heal clears mustHeal outside the sea so Register, List, and Transfer can run again.
230func Heal(cur realm, id int) {
231 caller := gate(cur, 0)
232 row := mustFish(id)
233 if row.owner != caller || row.inSea || row.onSale || !row.mustHeal || !row.revealed {
234 panic("seafish: heal")
235 }
236 row.mustHeal = false
237 row.hp = row.hpMax
238 chain.Emit("Heal", "id", strconv.Itoa(id))
239}
240
241// Transfer moves a fish the caller owns. A seated, staked, healing, or listed fish stays put.
242func Transfer(cur realm, to address, id int) {
243 from := gate(cur, 0)
244 move(cur, from, to, id)
245}
246
247// TransferFrom moves a fish. The caller must own it or be approved for it.
248func TransferFrom(cur realm, from, to address, id int) {
249 gate(cur, 0)
250 move(cur, from, to, id)
251}
252
253// Approve lets to move this fish once. The caller must own it. The call sends no coins.
254func Approve(cur realm, to address, id int) {
255 gate(cur, 0)
256 if err := teller.Approve(0, cur, to, tidOf(id)); err != nil {
257 panic("seafish: approve")
258 }
259}
260
261// SetApprovalForAll lets operator move every fish of the caller, or revokes it.
262func SetApprovalForAll(cur realm, operator address, approved bool) {
263 gate(cur, 0)
264 if err := teller.SetApprovalForAll(0, cur, operator, approved); err != nil {
265 panic("seafish: approve")
266 }
267}
268
269// List escrows the fish to this realm and puts it on sale. The caller sends no coins.
270// Refused while the fish is seated, still holds a stake, or must heal.
271func List(cur realm, id int, priceUgnot int64) {
272 caller := gate(cur, 0)
273 row := mustFish(id)
274 if priceUgnot <= 0 || priceUgnot > maxAsk || !row.revealed || row.onSale || row.owner != caller || busy(row) {
275 panic("seafish: list")
276 }
277 ledgerMove(caller, caller, self, id)
278 row.onSale = true
279 row.seller = caller
280 row.ask = priceUgnot
281 row.owner = self
282 chain.Emit("List", "id", strconv.Itoa(id), "seller", caller.String(), "price", strconv.FormatInt(priceUgnot, 10))
283}
284
285// UpdatePrice changes the ask of a fish the caller is selling.
286func UpdatePrice(cur realm, id int, priceUgnot int64) {
287 caller := gate(cur, 0)
288 row := mustFish(id)
289 if priceUgnot <= 0 || priceUgnot > maxAsk || !row.onSale || row.seller != caller {
290 panic("seafish: list")
291 }
292 row.ask = priceUgnot
293 chain.Emit("List", "id", strconv.Itoa(id), "seller", caller.String(), "price", strconv.FormatInt(priceUgnot, 10))
294}
295
296// Cancel returns a listed fish to its seller. The call sends no coins.
297func Cancel(cur realm, id int) {
298 caller := gate(cur, 0)
299 row := mustFish(id)
300 if !row.onSale || row.seller != caller {
301 panic("seafish: list")
302 }
303 ledgerMove(self, self, caller, id)
304 row.onSale = false
305 row.owner = caller
306 row.seller = address("")
307 row.ask = 0
308 chain.Emit("Cancel", "id", strconv.Itoa(id))
309}
310
311// Buy pays the ask, in full, to the seller and gives the fish to the buyer.
312func Buy(cur realm, id int) {
313 if !cur.Previous().IsUserCall() {
314 panic("seafish: eoa")
315 }
316 if !inited {
317 panic("seafish: init")
318 }
319 row := mustFish(id)
320 buyer := cur.Previous().Address()
321 if !row.onSale || buyer == row.seller {
322 panic("seafish: list")
323 }
324 price := row.ask
325 seller := row.seller
326 takeExact(price)
327 ledgerMove(self, self, buyer, id)
328 row.onSale = false
329 row.owner = buyer
330 row.seller = address("")
331 row.ask = 0
332 send(seller, price, cur)
333 chain.Emit("Buy", "id", strconv.Itoa(id), "buyer", buyer.String(), "price", strconv.FormatInt(price, 10))
334}
335
336// RegisterCollection publishes this realm on bazaarv5. The caller must be the Init wallet,
337// the same wallet that reserved slug seafish, and must send no coins. Call it once, after Init.
338func RegisterCollection(cur realm) {
339 caller := gate(cur, 0)
340 if caller != mintTo || market {
341 panic("seafish: admin")
342 }
343 factory.Register(cross(cur), "GnoFish Sea", "GSEA", cover, maxMint, mintFee, 0)
344 market = true
345 factory.NoteMinted(cross(cur), shown)
346}
347
348// Roll is the reveal hash. entropyH must be commitH+2. Trait adds 12%, half up.
349func Roll(caller address, commitH int64, entropyH int64, id int) (rarity string, trait string, bite int, hp int, speed int) {
350 if id < 1 || commitH < 1 || entropyH != commitH+2 {
351 panic("seafish: roll")
352 }
353 raw := caller.String() + "|" + strconv.FormatInt(commitH, 10) + "|" + strconv.FormatInt(entropyH, 10) + "|" + strconv.Itoa(id)
354 sum := sha256.Sum256([]byte(raw))
355 n := (int(sum[0])<<8 | int(sum[1])) % 10000
356 tier := 0
357 if n == 0 {
358 tier = 5
359 } else if n < 4 {
360 tier = 4
361 } else if n < 14 {
362 tier = 3
363 } else if n < 54 {
364 tier = 2
365 } else if n < 254 {
366 tier = 1
367 }
368 kind := int(sum[2]) % 3
369 bite = biteBase[tier]
370 hp = hpBase[tier]
371 speed = spdBase[tier]
372 if kind == 0 {
373 speed = (speed*112 + 50) / 100
374 } else if kind == 1 {
375 bite = (bite*112 + 50) / 100
376 } else {
377 hp = (hp*112 + 50) / 100
378 }
379 return rarities[tier], traits[kind], bite, hp, speed
380}
381
382func Realm() address { return self }
383func MintTo() address { return mintTo }
384func Liquidity() address { return lpSink }
385func Team() address { return team }
386func Version() string { return "seafish-1" }
387func Name() string { return "GnoFish Sea" }
388func GrcName() string { return Name() }
389func Symbol() string { return "GSEA" }
390func GrcSymbol() string { return Symbol() }
391func MintPrice() int64 { return mintFee }
392func StakeMin() int64 { return stakeMin }
393func MaxMint() int { return maxMint }
394func Minted() int { return minted }
395func Shown() int { return shown }
396func MarketOpen() bool { return market }
397func Height() int64 { return runtime.ChainHeight() }
398
399func TraitOf(id int) string { return mustFish(id).trait }
400func RarityOf(id int) string { return mustFish(id).rarity }
401func SizeOf(id int) string {
402 mustFish(id)
403 return "medium"
404}
405func BoutsOf(id int) int { return mustFish(id).bouts }
406func InMatch(id int) bool { return mustFish(id).inSea }
407func PoolOf(id int) int64 { return mustFish(id).pool }
408func Revealed(id int) bool { return mustFish(id).revealed }
409func MustHeal(id int) bool { return mustFish(id).mustHeal }
410func CommitOf(id int) int64 { return mustFish(id).commitH }
411func HpOf(id int) int { return mustFish(id).hp }
412func BiteOf(id int) int { return mustFish(id).bite }
413func SpeedOf(id int) int { return mustFish(id).speed }
414func NameOf(id int) string { return mustFish(id).name }
415func Listed(id int) bool { return revealedFish(id).onSale }
416func PriceOf(id int) int64 { return revealedFish(id).ask }
417func ImageOf(id int) string { return imageHTTPS + revealedFish(id).file }
418
419func SellerOf(id int) string {
420 row := revealedFish(id)
421 if !row.seller.IsValid() {
422 return ""
423 }
424 return row.seller.String()
425}
426
427func ItemLine(id int) string {
428 row := revealedFish(id)
429 line := "Species:" + speciesOf(row.rarity) + ";Rarity:" + row.rarity + ";Trait:" + row.trait + ";Bite:" + strconv.Itoa(row.bite) + ";HP:" + strconv.Itoa(row.hp) + ";Speed:" + strconv.Itoa(row.speed)
430 return strconv.Itoa(id) + "|" + row.name + "|" + row.owner.String() + "|" + SellerOf(id) + "|" + strconv.FormatInt(row.ask, 10) + "|" + strconv.FormatBool(row.onSale) + "|" + imageHTTPS + row.file + "|" + slug + "|" + row.rarity + "|" + line + "|true|-1"
431}
432
433func ListOpen() string {
434 if !inited {
435 return ""
436 }
437 out := ""
438 n := 0
439 for id := 1; id <= minted && n < listCap; id++ {
440 if !roster[id].onSale {
441 continue
442 }
443 if out != "" {
444 out += "\n"
445 }
446 out += ItemLine(id)
447 n++
448 }
449 return out
450}
451
452func TokensOf(owner address) string {
453 if !inited || !owner.IsValid() {
454 return ""
455 }
456 out := ""
457 for id := 1; id <= minted; id++ {
458 if roster[id].owner != owner {
459 continue
460 }
461 if out != "" {
462 out += ","
463 }
464 out += strconv.Itoa(id)
465 }
466 return out
467}
468
469func Pending(owner string) int {
470 who := address(owner)
471 if !who.IsValid() {
472 return 0
473 }
474 return pending(who)
475}
476
477func OwnerOf(tid grc721.TokenID) (address, error) {
478 if token == nil {
479 return "", errors.New("seafish: init")
480 }
481 return token.OwnerOf(tid)
482}
483
484func BalanceOf(addr address) (int64, error) {
485 if token == nil {
486 return 0, errors.New("seafish: init")
487 }
488 return token.BalanceOf(addr)
489}
490
491func TotalSupply() int64 {
492 if token == nil {
493 return 0
494 }
495 return token.TotalSupply()
496}
497
498func GetApproved(tid grc721.TokenID) (address, error) {
499 if token == nil {
500 return "", errors.New("seafish: init")
501 }
502 addr, err := token.GetApproved(tid)
503 if err == grc721.ErrTokenIdNotApproved {
504 return "", nil
505 }
506 return addr, err
507}
508
509func IsApprovedForAll(owner, operator address) bool {
510 if token == nil {
511 return false
512 }
513 return token.IsApprovedForAll(owner, operator)
514}
515
516func TokenByIndex(index int64) (grc721.TokenID, error) {
517 if indexed == nil {
518 return "", errors.New("seafish: init")
519 }
520 return indexed.TokenByIndex(index)
521}
522
523func TokenOfOwnerByIndex(owner address, index int64) (grc721.TokenID, error) {
524 if indexed == nil {
525 return "", errors.New("seafish: init")
526 }
527 return indexed.TokenOfOwnerByIndex(owner, index)
528}
529
530// TokenURI is the https image Adena puts in an img tag.
531func TokenURI(tid grc721.TokenID) (string, error) {
532 row, err := byToken(tid)
533 if err != nil {
534 return "", err
535 }
536 return imageHTTPS + row.file, nil
537}
538
539// TokenMetadata is the JSON Adena reads. Attribute keys are traitType.
540func TokenMetadata(tid grc721.TokenID) (string, error) {
541 row, err := byToken(tid)
542 if err != nil {
543 return "", err
544 }
545 return `{"name":"` + row.name + `","description":"` + row.name + `, a ` + row.rarity + ` GnoFish.","image":"` + imageHTTPS + row.file + `","attributes":[` +
546 attr("species", speciesOf(row.rarity)) + `,` +
547 attr("rarity", row.rarity) + `,` +
548 attr("trait", row.trait) + `,` +
549 attr("bite", strconv.Itoa(row.bite)) + `,` +
550 attr("hp", strconv.Itoa(row.hp)) + `,` +
551 attr("speed", strconv.Itoa(row.speed)) + `,` +
552 attr("ipfs", imageIPFS+row.file) + `]}`, nil
553}
554
555func Render(path string) string {
556 if path != "" {
557 return "# GnoFish Sea\n\nUnknown path.\n"
558 }
559 return "# GnoFish Sea\n\nPerson versus person. The mint fee goes to the deploy wallet. A win pays 80% to the winner, 10% to liquidity, and 10% to the team. Portraits are https images. A seated fish cannot be listed.\n"
560}
561
562func gate(cur realm, pay int64) address {
563 if !cur.Previous().IsUserCall() {
564 panic("seafish: eoa")
565 }
566 if !inited || self == address("") {
567 panic("seafish: init")
568 }
569 takeExact(pay)
570 return cur.Previous().Address()
571}
572
573func payMint(fee int64, rlm realm) {
574 send(mintTo, fee, rlm)
575}
576
577func splitKill(covered int64) (killer int64, lp int64, crew int64) {
578 killer = covered * 80 / 100
579 lp = covered * 10 / 100
580 crew = covered - killer - lp
581 return
582}
583
584func send(to address, amt int64, rlm realm) {
585 if !rlm.IsCurrent() {
586 panic("seafish: realm")
587 }
588 if amt == 0 {
589 return
590 }
591 bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)
592 bk.SendCoins(rlm.Address(), to, chain.Coins{{Denom: denom, Amount: amt}})
593}
594
595func noteSupply(cur realm) {
596 if !market {
597 return
598 }
599 factory.NoteMinted(cross(cur), shown)
600}
601
602func grow() int {
603 if minted >= maxMint || len(roster) != minted+1 {
604 panic("seafish: mint")
605 }
606 roster = append(roster, fish{})
607 minted++
608 return minted
609}
610
611func mustFish(id int) *fish {
612 if !inited || id < 1 || id > minted || id >= len(roster) {
613 panic("seafish: fish")
614 }
615 return &roster[id]
616}
617
618func revealedFish(id int) *fish {
619 row := mustFish(id)
620 if !row.revealed {
621 panic("seafish: uri")
622 }
623 return row
624}
625
626func pending(who address) int {
627 for i := 1; i <= minted; i++ {
628 if roster[i].owner == who && !roster[i].revealed {
629 return i
630 }
631 }
632 return 0
633}
634
635func seated(who address) bool {
636 for i := 1; i <= minted; i++ {
637 if roster[i].owner == who && roster[i].inSea {
638 return true
639 }
640 }
641 return false
642}
643
644func busy(row *fish) bool {
645 return row.inSea || row.pool != 0 || row.mustHeal
646}
647
648func pickName(tier int) string {
649 base := species[tier]
650 if !nameTaken(base) {
651 return base
652 }
653 for n := 2; n <= maxMint; n++ {
654 candidate := base + strconv.Itoa(n)
655 if !nameTaken(candidate) {
656 return candidate
657 }
658 }
659 panic("seafish: name")
660}
661
662func nameTaken(name string) bool {
663 for i := 1; i <= minted; i++ {
664 if roster[i].revealed && roster[i].name == name {
665 return true
666 }
667 }
668 return false
669}
670
671func tierOf(rarity string) int {
672 for i := 0; i < len(rarities); i++ {
673 if rarities[i] == rarity {
674 return i
675 }
676 }
677 panic("seafish: roll")
678}
679
680func move(cur realm, from, to address, id int) {
681 row := mustFish(id)
682 if row.onSale || !row.revealed || busy(row) || !to.IsValid() || to == from {
683 panic("seafish: transfer")
684 }
685 if err := teller.TransferFrom(0, cur, from, to, tidOf(id)); err != nil {
686 panic("seafish: transfer")
687 }
688 row.owner = to
689}
690
691func ledgerMove(spender, from, to address, id int) {
692 if err := ledger.TransferFrom(spender, from, to, tidOf(id)); err != nil {
693 panic("seafish: transfer")
694 }
695}
696
697func tidOf(id int) grc721.TokenID {
698 return grc721.TokenID(strconv.Itoa(id))
699}
700
701func byToken(tid grc721.TokenID) (*fish, error) {
702 if token == nil {
703 return nil, errors.New("seafish: init")
704 }
705 if _, err := token.OwnerOf(tid); err != nil {
706 return nil, err
707 }
708 id, err := strconv.Atoi(string(tid))
709 if err != nil || id < 1 || id > minted || id >= len(roster) || !roster[id].revealed {
710 return nil, errors.New("seafish: fish")
711 }
712 return &roster[id], nil
713}
714
715func speciesOf(rarity string) string {
716 for i := 0; i < len(rarities); i++ {
717 if rarities[i] == rarity {
718 return species[i]
719 }
720 }
721 return ""
722}
723
724func attr(key, value string) string {
725 return `{"traitType":"` + key + `","value":"` + value + `"}`
726}
727
728func takeExact(n int64) {
729 sent := unsafe.OriginSend()
730 if n == 0 {
731 if len(sent) != 0 {
732 panic("seafish: payment")
733 }
734 return
735 }
736 if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount != n {
737 panic("seafish: payment")
738 }
739}