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.

fish.gno

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