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.

store.gno

6.89 Kb · 223 lines
  1package guardians_store_v1
  2
  3// UNDEPLOYED. Selected inventory only; no minting or operator-transfer rights.
  4import (
  5	"chain"
  6	"chain/banker"
  7	"chain/runtime/unsafe"
  8	"strconv"
  9	"strings"
 10
 11	guardians "gno.land/r/g1nmqhha0ua730vejxqhrelldm4pga5j5ptle5ea/guardians"
 12)
 13
 14const Seller address = "g1a72vne647vcw9uv0gy7wn8sw8ffc66agl4jdr3"
 15const RoyaltyReceiver address = "g1lp547ww848djw0ammpqswegg7tc6xczqqtx07n"
 16const RoyaltyBps = int64(400)
 17const maxPrice = int64(9000000000000000) // multiplication by 400 stays in int64
 18
 19type order struct {
 20	nonce  int64
 21	price  int64
 22	active bool
 23}
 24
 25var orders = make(map[string]*order)
 26var nextNonce int64
 27var storeAddress address
 28var paused = true
 29
 30func init(cur realm)   { storeAddress = cur.Address() }
 31func Address() address { return storeAddress }
 32func Version() string  { return "guardians.selected-store.v1" }
 33func Paused() bool     { return paused }
 34func NextNonce() int64 { return nextNonce + 1 }
 35func Identity() string {
 36	return `{"version":"` + Version() + `","seller":"` + Seller.String() + `","royaltyReceiver":"` + RoyaltyReceiver.String() + `","royaltyBps":400,"address":"` + storeAddress.String() + `","paused":` + strconv.FormatBool(paused) + `}`
 37}
 38
 39// Public catalogue contains only active orders, never private seller inventory.
 40// A fresh Snapshot and Buy recheck custody before any wallet signs or settles.
 41func Listings() string {
 42	rows := []string{}
 43	for n := 1; n <= 3333; n++ {
 44		id := strconv.Itoa(n)
 45		o := orders[id]
 46		if o != nil && o.active {
 47			rows = append(rows, `{"tokenId":`+id+`,"nonce":`+strconv.FormatInt(o.nonce, 10)+`,"priceUgnot":`+strconv.FormatInt(o.price, 10)+`}`)
 48		}
 49	}
 50	return "[" + strings.Join(rows, ",") + "]"
 51}
 52func Snapshot(id string) string {
 53	n, err := strconv.Atoi(id)
 54	if err != nil || n < 1 || n > 3333 || strconv.Itoa(n) != id {
 55		panic("store: invalid token ID")
 56	}
 57	o := orders[id]
 58	base := `{"tokenId":` + id + `,"owner":"` + guardians.OwnerOf(id).String() + `","paused":` + strconv.FormatBool(paused)
 59	if o == nil {
 60		return base + `,"registered":false,"active":false,"nonce":0,"priceUgnot":0}`
 61	}
 62	return base + `,"registered":true,"active":` + strconv.FormatBool(o.active) + `,"nonce":` + strconv.FormatInt(o.nonce, 10) + `,"priceUgnot":` + strconv.FormatInt(o.price, 10) + `}`
 63}
 64func HasOrder(id string) bool    { return orders[id] != nil }
 65func OrderNonce(id string) int64 { return mustOrder(id).nonce }
 66func OrderPrice(id string) int64 { return mustOrder(id).price }
 67func IsListed(id string) bool {
 68	o := orders[id]
 69	return o != nil && o.active && guardians.OwnerOf(id) == storeAddress
 70}
 71
 72// Register BEFORE the seller directly transfers a selected NFT to this realm.
 73// The store cannot pull NFTs out of the seller's wallet.
 74func Register(cur realm, id string) {
 75	onlySeller(cur)
 76	noPayment()
 77	n, err := strconv.Atoi(id)
 78	if err != nil || n < 1 || n > 3333 || strconv.Itoa(n) != id {
 79		panic("store: invalid token ID")
 80	}
 81	if HasOrder(id) {
 82		panic("store: already registered")
 83	}
 84	if guardians.OwnerOf(id) != Seller {
 85		panic("store: seller does not own token")
 86	}
 87	orders[id] = &order{nonce: freshNonce()}
 88	chain.Emit("StoreRegistered", "tokenId", id)
 89}
 90
 91// Explicit positive price only. No default price and no free checkout.
 92func List(cur realm, id string, price, expectedNonce int64) {
 93	onlySeller(cur)
 94	noPayment()
 95	o := currentOrder(id, expectedNonce)
 96	if guardians.OwnerOf(id) != storeAddress {
 97		panic("store: token is not deposited")
 98	}
 99	if price < 1 || price > maxPrice {
100		panic("store: invalid price")
101	}
102	verifyRoyalty(id, price)
103	o.price = price
104	o.active = true
105	o.nonce = freshNonce()
106	chain.Emit("StoreListed", "tokenId", id, "price", strconv.FormatInt(price, 10), "nonce", strconv.FormatInt(o.nonce, 10))
107}
108
109func Unlist(cur realm, id string, expectedNonce int64) {
110	onlySeller(cur)
111	noPayment()
112	o := currentOrder(id, expectedNonce)
113	o.active = false
114	o.nonce = freshNonce()
115	chain.Emit("StoreUnlisted", "tokenId", id)
116}
117
118// Returns selected, unsold inventory even while globally paused. An undeployed
119// or unapproved realm must NEVER receive real NFTs.
120func Withdraw(cur realm, id string, expectedNonce int64) {
121	onlySeller(cur)
122	noPayment()
123	currentOrder(id, expectedNonce)
124	owner := guardians.OwnerOf(id)
125	if owner != storeAddress && owner != Seller {
126		panic("store: inventory owner changed")
127	}
128	delete(orders, id)
129	if owner == storeAddress {
130		guardians.TransferFrom(cross(cur), storeAddress, Seller, id)
131	}
132	chain.Emit("StoreWithdrawn", "tokenId", id)
133}
134
135func SetPaused(cur realm, value bool) {
136	onlySeller(cur)
137	noPayment()
138	paused = value
139	chain.Emit("StorePaused", "paused", strconv.FormatBool(value))
140}
141
142// One MsgCall settles the NFT and both payouts. Any abort rolls back the tx.
143// Expected nonce AND price stop a stale checkout accepting edited terms.
144func Buy(cur realm, id string, expectedNonce, expectedPrice int64) {
145	directUser(cur)
146	if paused {
147		panic("store: paused")
148	}
149	buyer := cur.Previous().Address()
150	if buyer == Seller {
151		panic("store: seller cannot buy")
152	}
153	o := currentOrder(id, expectedNonce)
154	if !o.active || o.price < 1 {
155		panic("store: not listed")
156	}
157	if o.price != expectedPrice {
158		panic("store: price changed")
159	}
160	if guardians.OwnerOf(id) != storeAddress {
161		panic("store: inventory owner changed")
162	}
163	sent := unsafe.OriginSend()
164	if len(sent) != 1 || sent[0].Denom != "ugnot" || sent[0].Amount != o.price {
165		panic("store: exact payment required")
166	}
167	fee := verifyRoyalty(id, o.price)
168	price := o.price
169	delete(orders, id)
170	guardians.TransferFrom(cross(cur), storeAddress, buyer, id)
171	b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
172	b.SendCoins(storeAddress, Seller, chain.NewCoins(chain.NewCoin("ugnot", price-fee)))
173	if fee > 0 {
174		b.SendCoins(storeAddress, RoyaltyReceiver, chain.NewCoins(chain.NewCoin("ugnot", fee)))
175	}
176	chain.Emit("StorePurchased", "tokenId", id, "buyer", buyer.String(), "price", strconv.FormatInt(price, 10), "royalty", strconv.FormatInt(fee, 10), "nonce", strconv.FormatInt(expectedNonce, 10))
177}
178
179func verifyRoyalty(id string, price int64) int64 {
180	receiver, bps := guardians.DefaultRoyalty()
181	actualReceiver, fee := guardians.RoyaltyInfo(id, price)
182	if receiver != RoyaltyReceiver || bps != RoyaltyBps || actualReceiver != RoyaltyReceiver || fee != price*RoyaltyBps/10000 {
183		panic("store: royalty policy changed")
184	}
185	return fee
186}
187func mustOrder(id string) *order {
188	o := orders[id]
189	if o == nil {
190		panic("store: order not found")
191	}
192	return o
193}
194func currentOrder(id string, nonce int64) *order {
195	o := mustOrder(id)
196	if o.nonce != nonce {
197		panic("store: stale order")
198	}
199	return o
200}
201func freshNonce() int64 {
202	if nextNonce >= 9223372036854775806 {
203		panic("store: nonce exhausted")
204	}
205	nextNonce++
206	return nextNonce
207}
208func directUser(cur realm) {
209	if !cur.Previous().IsUserCall() {
210		panic("store: direct user required")
211	}
212}
213func onlySeller(cur realm) {
214	directUser(cur)
215	if cur.Previous().Address() != Seller {
216		panic("store: seller only")
217	}
218}
219func noPayment() {
220	if len(unsafe.OriginSend()) != 0 {
221		panic("store: no payment expected")
222	}
223}