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}