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

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}