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

14.00 Kb · 675 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 fish2
  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	portrait     string
102	colName      string
103	colSymbol    string
104)
105
106func Init(cur realm, name, symbol, images string, royalty int64, upgrade, lp, burn, protocol, season address) {
107	if !cur.IsCurrent() {
108		panic("fish: realm")
109	}
110	if !cur.Previous().IsUserCall() {
111		panic("fish: eoa")
112	}
113	if inited {
114		panic("fish: init")
115	}
116	takeExact(0)
117	if !validCollectionName(name) || !validSymbol(symbol) {
118		panic("fish: name")
119	}
120	if !strings.HasPrefix(images, "https://") || strings.Contains(images, "|") || len(images) > 160 {
121		panic("fish: image")
122	}
123	if royalty < 0 || royalty > maxRoyalty {
124		panic("fish: royalty")
125	}
126	if !upgrade.IsValid() || !lp.IsValid() || !burn.IsValid() || !protocol.IsValid() || !season.IsValid() {
127		panic("fish: sink")
128	}
129	tok, led := grc721.NewToken(name, symbol, tokenSeq.Next(), cur)
130	token = tok
131	ledger = led
132	metaView, metaLed = metadata.NewMetadata(led)
133	creator = cur.Previous().Address()
134	self = cur.Address()
135	upgradeSink = upgrade
136	lpSink = lp
137	burnSink = burn
138	protocolSink = protocol
139	seasonSink = season
140	royaltyBps = royalty
141	imageBase = images
142	colName = name
143	colSymbol = symbol
144	bodies = []*fishBody{nil}
145	nextID = 0
146	minted = 0
147	inited = true
148	chain.Emit("Init", "creator", creator.String(), "symbol", symbol)
149}
150
151func Mint(cur realm, name, size, trait string) int {
152	caller := requireUser(cur)
153	if unplayed(caller) {
154		panic("fish: unplayed")
155	}
156	if !validFishName(name) {
157		panic("fish: name")
158	}
159	if !validSize(size) {
160		panic("fish: size")
161	}
162	if !validTrait(trait) {
163		panic("fish: trait")
164	}
165	takeExact(mintFee)
166	payMint(mintFee, cur)
167	return issue(caller, name, "common", size, trait)
168}
169
170// SetArt sets one https image on every unburned fish. The creator may call it again.
171func SetArt(cur realm, image string) {
172	caller := requireUser(cur)
173	requireInit()
174	if caller != creator {
175		panic("fish: owner")
176	}
177	takeExact(0)
178	if !validArt(image) {
179		panic("fish: image")
180	}
181	portrait = image
182	for _, body := range bodies {
183		if body == nil || body.burned {
184			continue
185		}
186		body.image = image
187		writeURI(body)
188	}
189	chain.Emit("SetArt", "image", image)
190}
191
192func Portrait() string {
193	requireInit()
194	return portrait
195}
196
197func Transfer(cur realm, to address, id int) {
198	caller := requireUser(cur)
199	takeExact(0)
200	if !to.IsValid() || to == caller || to == self {
201		panic("fish: addr")
202	}
203	body := must(id)
204	if body.owner != caller {
205		panic("fish: owner")
206	}
207	guardMovable(body)
208	moveToken(caller, caller, to, id)
209	body.owner = to
210}
211
212func TransferFrom(cur realm, from, to, tid string) {
213	caller := requireUser(cur)
214	takeExact(0)
215	id := parseID(tid)
216	src := address(from)
217	dst := address(to)
218	if !src.IsValid() || !dst.IsValid() || dst == self {
219		panic("fish: addr")
220	}
221	body := must(id)
222	if body.owner != src {
223		panic("fish: owner")
224	}
225	guardMovable(body)
226	moveToken(caller, src, dst, id)
227	body.owner = dst
228}
229
230func Approve(cur realm, to, tid string) {
231	caller := requireUser(cur)
232	takeExact(0)
233	id := parseID(tid)
234	body := must(id)
235	guardMovable(body)
236	who := address(to)
237	if to != "" && !who.IsValid() {
238		panic("fish: addr")
239	}
240	if err := ledger.Approve(caller, who, tokenID(id)); err != nil {
241		panic("fish: approve")
242	}
243}
244
245func SetApprovalForAll(cur realm, operator string, approved bool) {
246	caller := requireUser(cur)
247	takeExact(0)
248	op := address(operator)
249	if !op.IsValid() {
250		panic("fish: addr")
251	}
252	if err := ledger.SetApprovalForAll(caller, op, approved); err != nil {
253		panic("fish: approve")
254	}
255}
256
257func OwnerOf(tid string) (address, error) {
258	requireInit()
259	id, err := strconv.Atoi(tid)
260	if err != nil || id < 1 || id >= len(bodies) || bodies[id] == nil || bodies[id].burned {
261		return address(""), errors.New("fish: unknown")
262	}
263	return bodies[id].owner, nil
264}
265
266func Name() string {
267	requireInit()
268	return token.GetName()
269}
270
271func Symbol() string {
272	requireInit()
273	return token.GetSymbol()
274}
275
276func GrcName() string   { return Name() }
277func GrcSymbol() string { return Symbol() }
278
279func BalanceOf(addr string) int64 {
280	requireInit()
281	who := address(addr)
282	if !who.IsValid() {
283		return 0
284	}
285	n, err := token.BalanceOf(who)
286	if err != nil {
287		return 0
288	}
289	return n
290}
291
292func GrcBalanceOf(addr string) int64 { return BalanceOf(addr) }
293
294func TokenURI(tid string) string {
295	id := parseID(tid)
296	must(id)
297	uri, err := metaView.TokenURI(tokenID(id))
298	if err != nil || uri == "" {
299		panic("fish: uri")
300	}
301	return uri
302}
303
304func TokensOf(owner string) string {
305	requireInit()
306	who := address(owner)
307	if !who.IsValid() {
308		return ""
309	}
310	out := ""
311	for _, body := range bodies {
312		if body == nil || body.burned || body.owner != who {
313			continue
314		}
315		if out != "" {
316			out += ","
317		}
318		out += strconv.Itoa(body.id)
319	}
320	return out
321}
322
323func TokenOfOwnerByIndex(owner string, index int64) string {
324	requireInit()
325	who := address(owner)
326	if !who.IsValid() {
327		panic("fish: addr")
328	}
329	if index < 0 {
330		panic("fish: index")
331	}
332	var seen int64
333	for _, body := range bodies {
334		if body == nil || body.burned || body.owner != who {
335			continue
336		}
337		if seen == index {
338			return strconv.Itoa(body.id)
339		}
340		seen++
341	}
342	panic("fish: index")
343}
344
345func TotalSupply() int64 {
346	requireInit()
347	return token.TotalSupply()
348}
349
350func GetApproved(id int) string {
351	requireInit()
352	who, err := token.GetApproved(tokenID(id))
353	if err != nil || !who.IsValid() {
354		return ""
355	}
356	return who.String()
357}
358
359func IsApprovedForAll(owner, operator string) bool {
360	requireInit()
361	a := address(owner)
362	b := address(operator)
363	if !a.IsValid() || !b.IsValid() {
364		return false
365	}
366	return token.IsApprovedForAll(a, b)
367}
368
369func MintPrice() int64   { return mintFee }
370func MaxSupply() int     { return 0 }
371func Minted() int        { return minted }
372func RoyaltyBps() int64  { return royaltyBps }
373func Creator() address   { return creator }
374func ProtocolBps() int64 { return protocolBps }
375
376func BoutsOf(id int) int      { return must(id).bouts }
377func PoolOf(id int) int64     { return must(id).pool }
378func Listed(id int) bool      { return must(id).listed }
379func InMatch(id int) bool     { return must(id).inMatch }
380func LayOpen(id int) bool     { return must(id).layOpen }
381func RarityOf(id int) string  { return must(id).rarity }
382func SizeOf(id int) string    { return must(id).size }
383func TraitOf(id int) string   { return must(id).trait }
384func SpeciesOf(id int) string { return must(id).species }
385
386func issue(to address, name, rarity, size, trait string) int {
387	nextID++
388	id := nextID
389	maxHP := hpOf(rarity, size, trait)
390	body := &fishBody{
391		id:      id,
392		name:    name,
393		rarity:  rarity,
394		species: speciesOf(rarity),
395		size:    size,
396		trait:   trait,
397		hp:      maxHP,
398		hpNow:   maxHP,
399		dodge:           dodgeOf(size),
400		faceDx:          0,
401		faceDz:          -1,
402		aspectBps:       10000,
403		springBorn:      -1,
404		lastCombatMilli: -1,
405		owner:           to,
406		image:   artOf(rarity),
407	}
408	for len(bodies) <= id {
409		bodies = append(bodies, nil)
410	}
411	bodies[id] = body
412	minted++
413	mintGRC(to, body)
414	return id
415}
416
417func mintGRC(to address, body *fishBody) {
418	tid := tokenID(body.id)
419	if err := ledger.Mint(to, tid); err != nil {
420		panic("fish: mint")
421	}
422	writeURI(body)
423}
424
425func writeURI(body *fishBody) {
426	if err := metaLed.SetTokenURI(tokenID(body.id), tokenJSON(body)); err != nil {
427		panic("fish: uri")
428	}
429}
430
431func artOf(rarity string) string {
432	if portrait != "" {
433		return portrait
434	}
435	return imageBase + "/" + rarity + ".png"
436}
437
438func burnGRC(id int) {
439	if err := ledger.Burn(tokenID(id)); err != nil {
440		panic("fish: burn")
441	}
442}
443
444func moveToken(spender, from, to address, id int) {
445	if err := ledger.TransferFrom(spender, from, to, tokenID(id)); err != nil {
446		panic("fish: transfer")
447	}
448}
449
450func tokenJSON(body *fishBody) string {
451	return "data:application/json;charset=utf-8," +
452		`{"name":"` + body.name + `","description":"Shoal fish","image":"` + body.image +
453		`","attributes":[{"trait_type":"Species","value":"` + body.species +
454		`"},{"trait_type":"Size","value":"` + body.size +
455		`"},{"trait_type":"Trait","value":"` + body.trait +
456		`"},{"trait_type":"Rarity","value":"` + body.rarity +
457		`"},{"trait_type":"HP","value":"` + strconv.Itoa(body.hp) + `"}]}`
458}
459
460func requireUser(cur realm) address {
461	if !cur.IsCurrent() {
462		panic("fish: realm")
463	}
464	if !cur.Previous().IsUserCall() {
465		panic("fish: eoa")
466	}
467	return cur.Previous().Address()
468}
469
470func requireInit() {
471	if !inited || token == nil {
472		panic("fish: init")
473	}
474}
475
476func takeExact(n int64) {
477	sent := unsafe.OriginSend()
478	if n == 0 {
479		if len(sent) != 0 {
480			panic("fish: payment")
481		}
482		return
483	}
484	if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount != n {
485		panic("fish: payment")
486	}
487}
488
489func payMint(fee int64, rlm realm) {
490	up := fee * 7000 / 10000
491	lp := fee * 2000 / 10000
492	burn := fee - up - lp
493	send(upgradeSink, up, rlm)
494	send(lpSink, lp, rlm)
495	send(burnSink, burn, rlm)
496}
497
498func send(to address, amt int64, rlm realm) {
499	if amt == 0 {
500		return
501	}
502	if !to.IsValid() {
503		panic("fish: sink")
504	}
505	bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)
506	bk.SendCoins(rlm.Address(), to, chain.Coins{{Denom: denom, Amount: amt}})
507}
508
509func must(id int) *fishBody {
510	requireInit()
511	if id < 1 || id >= len(bodies) || bodies[id] == nil || bodies[id].burned {
512		panic("fish: unknown")
513	}
514	return bodies[id]
515}
516
517func guardMovable(body *fishBody) {
518	if body.listed {
519		panic("fish: listed")
520	}
521	if body.inMatch {
522		panic("fish: match")
523	}
524	if body.pool != 0 {
525		panic("fish: pool")
526	}
527	if body.layOpen {
528		panic("fish: lay")
529	}
530}
531
532func unplayed(owner address) bool {
533	for _, body := range bodies {
534		if body == nil || body.burned || body.bouts >= 1 {
535			continue
536		}
537		if body.owner == owner || (body.listed && body.seller == owner) {
538			return true
539		}
540	}
541	return false
542}
543
544func tokenID(id int) grc721.TokenID {
545	return grc721.TokenID(strconv.Itoa(id))
546}
547
548func parseID(tid string) int {
549	id, err := strconv.Atoi(tid)
550	if err != nil || id < 1 {
551		panic("fish: unknown")
552	}
553	return id
554}
555
556func validFishName(name string) bool {
557	if len(name) < 1 || len(name) > 20 {
558		return false
559	}
560	for _, c := range name {
561		if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != ' ' {
562			return false
563		}
564	}
565	return true
566}
567
568func validCollectionName(name string) bool {
569	return name != "" && len(name) <= 64 && !strings.Contains(name, "|")
570}
571
572func validArt(image string) bool {
573	if !strings.HasPrefix(image, "https://") || len(image) > 160 || strings.Contains(image, "|") {
574		return false
575	}
576	if strings.Contains(image, `"`) || strings.Contains(image, "\n") || strings.Contains(image, "\\") {
577		return false
578	}
579	return true
580}
581
582func validSymbol(symbol string) bool {
583	if symbol == "" || len(symbol) > grc721.MaxSymbolLen {
584		return false
585	}
586	for _, c := range symbol {
587		if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') {
588			return false
589		}
590	}
591	return true
592}
593
594func validSize(size string) bool {
595	return size == "small" || size == "medium" || size == "large"
596}
597
598func validTrait(trait string) bool {
599	return trait == "swift" || trait == "fang" || trait == "hide"
600}
601
602func speciesOf(rarity string) string {
603	switch rarity {
604	case "common":
605		return "Minnow"
606	case "uncommon":
607		return "Mackerel"
608	case "rare":
609		return "Snapper"
610	case "epic":
611		return "Angler"
612	case "legendary":
613		return "Koi Dragon"
614	case "mythic":
615		return "Abyss Leviathan"
616	default:
617		panic("fish: rarity")
618	}
619}
620
621func hpBase(rarity string) int {
622	switch rarity {
623	case "common":
624		return 100
625	case "uncommon":
626		return 112
627	case "rare":
628		return 126
629	case "epic":
630		return 142
631	case "legendary":
632		return 160
633	case "mythic":
634		return 180
635	default:
636		panic("fish: rarity")
637	}
638}
639
640func hpOf(rarity, size, trait string) int {
641	hp := hpBase(rarity)
642	if size == "small" {
643		hp = hp * 88 / 100
644	}
645	if size == "large" {
646		hp = hp * 120 / 100
647	}
648	if trait == "hide" {
649		hp = hp * 112 / 100
650	}
651	if hp < 1 {
652		hp = 1
653	}
654	return hp
655}
656
657func dodgeOf(size string) int {
658	if size == "small" {
659		return 24
660	}
661	return 0
662}
663
664func arenaStake(stake int64) bool {
665	if stake >= 5*ugnot && stake < 20*ugnot {
666		return true
667	}
668	if stake >= 20*ugnot && stake < 80*ugnot {
669		return true
670	}
671	if stake >= 80*ugnot && stake < 300*ugnot {
672		return true
673	}
674	return stake >= 300*ugnot && stake <= 1000*ugnot
675}